Swift Testing for Unit Tests PART 1 (#5119)

* migrated a lot of unit tests to Swift Testing and added a new implementation for deferred fulfillment

more tests migration

Cleaned the code manually to establish some good patterns

more code improvements

some more code improvements

removed empty tests

update project

* more pr suggestions and cleanups

* removed the TestSetup pattern

* fixing claude not reusing tests

* pr suggestion + added indent rule to swiftformat so that we can prevent AIs to change that
This commit is contained in:
Mauro
2026-02-19 16:20:47 +01:00
committed by GitHub
parent c92e847ed7
commit 173b39a07f
118 changed files with 4630 additions and 4129 deletions

View File

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

View File

@@ -52,7 +52,6 @@
0638CBDE3098B1C3F23AFCFA /* MXLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 111B698739E3410E2CDB7144 /* MXLog.swift */; }; 0638CBDE3098B1C3F23AFCFA /* MXLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 111B698739E3410E2CDB7144 /* MXLog.swift */; };
065EAB39F3F3AB4F6BD2A362 /* AppLockSetupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19DD166C3625EE426203FA29 /* AppLockSetupTests.swift */; }; 065EAB39F3F3AB4F6BD2A362 /* AppLockSetupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19DD166C3625EE426203FA29 /* AppLockSetupTests.swift */; };
066A1E9B94723EE9F3038044 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.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 */; }; 06B55882911B4BF5B14E9851 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227AC5D71A4CE43512062243 /* URL.swift */; };
06D17F7813AA931FF18FD5D0 /* SDKListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5CD2993048222B64C45006 /* SDKListener.swift */; }; 06D17F7813AA931FF18FD5D0 /* SDKListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5CD2993048222B64C45006 /* SDKListener.swift */; };
06D3942496E9E0E655F14D21 /* NotificationManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.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 */; }; 1B2F9F368619FFF8C63C87CC /* BugReportScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EECE8B331CD169790EF284F /* BugReportScreenViewModelTests.swift */; };
1B5B30839656AE2F957C6B1E /* test_pdf.pdf in Resources */ = {isa = PBXBuildFile; fileRef = BE98688578F8B0541D853695 /* test_pdf.pdf */; }; 1B5B30839656AE2F957C6B1E /* test_pdf.pdf in Resources */ = {isa = PBXBuildFile; fileRef = BE98688578F8B0541D853695 /* test_pdf.pdf */; };
1B88BB631F7FC45A213BB554 /* TimelineItemSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55AEEF8142DF1B59DB40FB93 /* TimelineItemSender.swift */; }; 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 */; }; 1BA04D05EBC6646958B1BE60 /* PlaceholderScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF34A2FD6797535C95AC918D /* PlaceholderScreenCoordinator.swift */; };
1BEADA694AC53ABB8B459F9A /* LeaveSpaceRoomDetailsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC3797A2325BE44FFB478BE9 /* LeaveSpaceRoomDetailsCell.swift */; }; 1BEADA694AC53ABB8B459F9A /* LeaveSpaceRoomDetailsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC3797A2325BE44FFB478BE9 /* LeaveSpaceRoomDetailsCell.swift */; };
1C1750C009F7214B967928BC /* ManageRoomMemberSheetViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80807B554CF9C524F98674F /* ManageRoomMemberSheetViewModelTests.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 */; }; 3582056513A384F110EC8274 /* MediaPlayerProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7A2C4A3A74F0D2FFE9356A /* MediaPlayerProviderTests.swift */; };
35E975CFDA60E05362A7CF79 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 1222DB76B917EB8A55365BA5 /* target.yml */; }; 35E975CFDA60E05362A7CF79 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 1222DB76B917EB8A55365BA5 /* target.yml */; };
36206F74DDEBF9BEAF6A6A1F /* ExtensionLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A8571A8A071FB41778C016 /* ExtensionLogger.swift */; }; 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 */; }; 3684AD01C5FCB7616B28F629 /* TimelineMediaPreviewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDE60FEE95039CCCEEEE3B0 /* TimelineMediaPreviewController.swift */; };
36926D795D6D19177C7812F8 /* EncryptionResetPasswordScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6935A55AB3B0C94BC566DD6 /* EncryptionResetPasswordScreenCoordinator.swift */; }; 36926D795D6D19177C7812F8 /* EncryptionResetPasswordScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6935A55AB3B0C94BC566DD6 /* EncryptionResetPasswordScreenCoordinator.swift */; };
369BF960E52BBEE61F8A5BD1 /* BlockedUsersScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.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 */; }; 5AC5CD6D893073EE4D9A277E /* ShareExtensionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D27299A36536DBF91AE8FA6 /* ShareExtensionViewController.swift */; };
5AE6404C4FD4848ACCFF9EDC /* SecureBackupLogoutConfirmationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1573D28C8A9FB6399D0EEFB /* SecureBackupLogoutConfirmationScreenCoordinator.swift */; }; 5AE6404C4FD4848ACCFF9EDC /* SecureBackupLogoutConfirmationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1573D28C8A9FB6399D0EEFB /* SecureBackupLogoutConfirmationScreenCoordinator.swift */; };
5B6E5AD224509E6C0B520D6E /* RoomMemberDetailsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DDF49CEBC0DFC59C308335F /* RoomMemberDetailsScreenViewModelProtocol.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 */; }; 5BC6C4ADFE7F2A795ECDE130 /* SecureBackupKeyBackupScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2D4EEBE8C098BBADD10939 /* SecureBackupKeyBackupScreenCoordinator.swift */; };
5C02841B2A86327B2C377682 /* NotificationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C830A64609CBD152F06E0457 /* NotificationConstants.swift */; }; 5C02841B2A86327B2C377682 /* NotificationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C830A64609CBD152F06E0457 /* NotificationConstants.swift */; };
5C164551F7D26E24F09083D3 /* StaticLocationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C616D90B1E2F033CAA325439 /* StaticLocationScreenViewModelProtocol.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 */; }; 763D69741D58D2B650BC1FC9 /* CallScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F37FA1A5D55633E1942B153B /* CallScreenCoordinator.swift */; };
7640A4B412CACF15D143CCD4 /* Strings+SAS.swift in Sources */ = {isa = PBXBuildFile; fileRef = B172057567E049007A5C4D92 /* Strings+SAS.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 */; }; 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 */; }; 76C874243A8C440D6CF7B344 /* ProcessInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077B01C13BBA2996272C5FB5 /* ProcessInfo.swift */; };
7708976CEE6AFB5CFAEFBA68 /* PillTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CF1EE0AA78470C674554262 /* PillTextAttachment.swift */; }; 7708976CEE6AFB5CFAEFBA68 /* PillTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CF1EE0AA78470C674554262 /* PillTextAttachment.swift */; };
77574A519A4E484880053EAD /* IdentityConfirmationScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FDF541AE914059942B575B4 /* IdentityConfirmationScreenModels.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 */; }; 804C15D8ADE0EA7A5268F58A /* OverridableAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648DD1C10E4957CB791FE0B8 /* OverridableAvatarImage.swift */; };
80DEA2A4B20F9E279EAE6B2B /* UserProfile+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAD01F7FC2BBAC7351948595 /* UserProfile+Mock.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 */; }; 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 */; }; 81CFE6FE42DF26BBCEDC7FF2 /* JoinCallButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98ABC939BC8F08CA3E967D6C /* JoinCallButton.swift */; };
81D4E550668B230A63B26CFB /* SpacesScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB98BFD8E93C7FCCEDEC46F9 /* SpacesScreenViewModel.swift */; }; 81D4E550668B230A63B26CFB /* SpacesScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB98BFD8E93C7FCCEDEC46F9 /* SpacesScreenViewModel.swift */; };
8285FF4B2C2331758C437FF7 /* ReportContentScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 713B48DBF65DE4B0DD445D66 /* ReportContentScreenViewModelProtocol.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 */; }; 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 */; }; 85F89F3F320F4FADCFFFE68B /* ServerSelectionScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3059CFA00C67D8787273B20 /* ServerSelectionScreenViewModel.swift */; };
864C0D3A4077BF433DBC691F /* PollRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5281C5CDC4A712265A0B5FBF /* PollRoomTimelineItem.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 */; }; 8658F5034EAD7357CE7F9AC7 /* MatrixUserShareLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E31AB0E77BB70E2BC77463 /* MatrixUserShareLink.swift */; };
865DD5CA474C6AE6C2BC008E /* NetworkMonitorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1575947B7A6FE08C57FE5EE4 /* NetworkMonitorProtocol.swift */; }; 865DD5CA474C6AE6C2BC008E /* NetworkMonitorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1575947B7A6FE08C57FE5EE4 /* NetworkMonitorProtocol.swift */; };
86675910612A12409262DFBD /* SessionVerificationStateMachineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.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 */; }; 8A83D715940378B9BA9F739E /* RoomInviterLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EB58E4E8D6D634C246AD5C2 /* RoomInviterLabel.swift */; };
8AA84EF202F2EFC8453A97BD /* SecureBackupRecoveryKeyScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 645E027C112740573D27765C /* SecureBackupRecoveryKeyScreenModels.swift */; }; 8AA84EF202F2EFC8453A97BD /* SecureBackupRecoveryKeyScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 645E027C112740573D27765C /* SecureBackupRecoveryKeyScreenModels.swift */; };
8AB8ED1051216546CB35FA0E /* UserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5E9C044BEB7C70B1378E91 /* UserSession.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 */; }; 8B1D5CE017EEC734CF5FE130 /* Encodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260004737C573A56FA01E86E /* Encodable.swift */; };
8B408C574E35E1C9B43A50CE /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 048A21188AB19349D026BECD /* PrivacyInfo.xcprivacy */; }; 8B408C574E35E1C9B43A50CE /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 048A21188AB19349D026BECD /* PrivacyInfo.xcprivacy */; };
8B41D0357B91CD3B6F6A3BCA /* EmoteRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */; }; 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 */; }; F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */; };
F71C2B24AFB566119ACCDDA1 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3557ACB95D0F666EF5AF0CE /* Secrets.swift */; }; F71C2B24AFB566119ACCDDA1 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3557ACB95D0F666EF5AF0CE /* Secrets.swift */; };
F7567DD6635434E8C563BF85 /* AnalyticsClientProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.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 */; }; F777C6FEE7D106136E2ED2B2 /* MessageForwardingScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F6E6EDC4BBF962B2ED595A4 /* MessageForwardingScreenViewModelTests.swift */; };
F78BAD28482A467287A9A5A3 /* EventBasedMessageTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0900BBF0A5D5D775E917C70 /* EventBasedMessageTimelineItemProtocol.swift */; }; F78BAD28482A467287A9A5A3 /* EventBasedMessageTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0900BBF0A5D5D775E917C70 /* EventBasedMessageTimelineItemProtocol.swift */; };
F7932A3F075B0D3F24DEECB5 /* VoiceMessagePreviewComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE807361805463F5AEDD1CA /* VoiceMessagePreviewComposer.swift */; }; F7932A3F075B0D3F24DEECB5 /* VoiceMessagePreviewComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE807361805463F5AEDD1CA /* VoiceMessagePreviewComposer.swift */; };
@@ -1827,7 +1820,6 @@
2DA4F09CB613C54FDC73AE6A /* ThreadDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadDecorator.swift; sourceTree = "<group>"; }; 2DA4F09CB613C54FDC73AE6A /* ThreadDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadDecorator.swift; sourceTree = "<group>"; };
2DB0E533508094156D8024C3 /* TimelineStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyler.swift; sourceTree = "<group>"; }; 2DB0E533508094156D8024C3 /* TimelineStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyler.swift; sourceTree = "<group>"; };
2E11E7C396ED06A154CF6DF3 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/SAS.strings; sourceTree = "<group>"; }; 2E11E7C396ED06A154CF6DF3 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/SAS.strings; sourceTree = "<group>"; };
2E88534A39781D76487D59DF /* SecureBackupKeyBackupScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenViewModelTests.swift; sourceTree = "<group>"; };
2F06F70B9C433BAD4BC6B9F5 /* EncryptedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedRoomTimelineView.swift; sourceTree = "<group>"; }; 2F06F70B9C433BAD4BC6B9F5 /* EncryptedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedRoomTimelineView.swift; sourceTree = "<group>"; };
2F36C5D9B37E50915ECBD3EE /* RoomMemberProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxy.swift; sourceTree = "<group>"; }; 2F36C5D9B37E50915ECBD3EE /* RoomMemberProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxy.swift; sourceTree = "<group>"; };
2F926D08EB3D622A480BCA71 /* TimelineEventContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineEventContent.swift; sourceTree = "<group>"; }; 2F926D08EB3D622A480BCA71 /* TimelineEventContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineEventContent.swift; sourceTree = "<group>"; };
@@ -1906,7 +1898,6 @@
3EF1AC723C2609C7705569CA /* MediaLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoaderTests.swift; sourceTree = "<group>"; }; 3EF1AC723C2609C7705569CA /* MediaLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoaderTests.swift; sourceTree = "<group>"; };
3F54FA7C5CB7B342EF9B9B2F /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = "<group>"; }; 3F54FA7C5CB7B342EF9B9B2F /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = "<group>"; };
40076C770A5FB83325252973 /* VoiceMessageMediaManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageMediaManager.swift; sourceTree = "<group>"; }; 40076C770A5FB83325252973 /* VoiceMessageMediaManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageMediaManager.swift; sourceTree = "<group>"; };
40316EFFEAC7B206EE9A55AE /* SecureBackupScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreenViewModelTests.swift; sourceTree = "<group>"; };
4048547AC50ADCF201684E87 /* EditRoomAddressScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditRoomAddressScreen.swift; sourceTree = "<group>"; }; 4048547AC50ADCF201684E87 /* EditRoomAddressScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditRoomAddressScreen.swift; sourceTree = "<group>"; };
406C90AF8C3E98DF5D4E5430 /* ElementCallServiceConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallServiceConstants.swift; sourceTree = "<group>"; }; 406C90AF8C3E98DF5D4E5430 /* ElementCallServiceConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallServiceConstants.swift; sourceTree = "<group>"; };
407C8DD85179D2DB896FC0FA /* RoomFlowCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomFlowCoordinatorStateMachine.swift; sourceTree = "<group>"; }; 407C8DD85179D2DB896FC0FA /* RoomFlowCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomFlowCoordinatorStateMachine.swift; sourceTree = "<group>"; };
@@ -2140,10 +2131,8 @@
6C9651CD1066F239C7739240 /* NSEUserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEUserSession.swift; sourceTree = "<group>"; }; 6C9651CD1066F239C7739240 /* NSEUserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEUserSession.swift; sourceTree = "<group>"; };
6CD4823EAB4B4E8BAB4F6B8C /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = "<group>"; }; 6CD4823EAB4B4E8BAB4F6B8C /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = "<group>"; };
6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsScreenIdentifier.swift; sourceTree = "<group>"; }; 6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsScreenIdentifier.swift; sourceTree = "<group>"; };
6D0A27607AB09784C8501B5C /* DeveloperOptionsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenViewModelTests.swift; sourceTree = "<group>"; };
6DB53055CB130F0651C70763 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = "<group>"; }; 6DB53055CB130F0651C70763 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = "<group>"; };
6DF438EAFC732D2D95D34BF6 /* StartChatViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatViewModelTests.swift; sourceTree = "<group>"; }; 6DF438EAFC732D2D95D34BF6 /* StartChatViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatViewModelTests.swift; sourceTree = "<group>"; };
6E5725BC6C63604CB769145B /* LegalInformationScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenViewModelTests.swift; sourceTree = "<group>"; };
6E5E9C044BEB7C70B1378E91 /* UserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSession.swift; sourceTree = "<group>"; }; 6E5E9C044BEB7C70B1378E91 /* UserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSession.swift; sourceTree = "<group>"; };
6E964AF2DFEB31E2B799999F /* RoomPollsHistoryScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenModels.swift; sourceTree = "<group>"; }; 6E964AF2DFEB31E2B799999F /* RoomPollsHistoryScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenModels.swift; sourceTree = "<group>"; };
6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BindableState.swift; sourceTree = "<group>"; }; 6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BindableState.swift; sourceTree = "<group>"; };
@@ -2611,7 +2600,6 @@
C08E9043618AE5B0BF7B07E1 /* TemplateScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModelTests.swift; sourceTree = "<group>"; }; C08E9043618AE5B0BF7B07E1 /* TemplateScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModelTests.swift; sourceTree = "<group>"; };
C0900BBF0A5D5D775E917C70 /* EventBasedMessageTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedMessageTimelineItemProtocol.swift; sourceTree = "<group>"; }; C0900BBF0A5D5D775E917C70 /* EventBasedMessageTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedMessageTimelineItemProtocol.swift; sourceTree = "<group>"; };
C0FEA560929DD73FFEF8C3DF /* HomeScreenEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenEmptyStateView.swift; sourceTree = "<group>"; }; C0FEA560929DD73FFEF8C3DF /* HomeScreenEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenEmptyStateView.swift; sourceTree = "<group>"; };
C0FF08D0BD7D0B4B6877AB7D /* SecureBackupRecoveryKeyScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenViewModelTests.swift; sourceTree = "<group>"; };
C11397904D19CFF0E3689F0E /* SpaceScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceScreenModels.swift; sourceTree = "<group>"; }; C11397904D19CFF0E3689F0E /* SpaceScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceScreenModels.swift; sourceTree = "<group>"; };
C142248014E08E885E323E56 /* Avatars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Avatars.swift; sourceTree = "<group>"; }; C142248014E08E885E323E56 /* Avatars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Avatars.swift; sourceTree = "<group>"; };
C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDirection.swift; sourceTree = "<group>"; }; C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDirection.swift; sourceTree = "<group>"; };
@@ -2627,6 +2615,7 @@
C3285BD95B564CA2A948E511 /* OnboardingFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingFlowCoordinator.swift; sourceTree = "<group>"; }; C3285BD95B564CA2A948E511 /* OnboardingFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingFlowCoordinator.swift; sourceTree = "<group>"; };
C33B3F17996DFDF5F0181512 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = "<group>"; }; C33B3F17996DFDF5F0181512 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = "<group>"; };
C352359663A0E52BA20761EE /* LoadableImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableImage.swift; sourceTree = "<group>"; }; C352359663A0E52BA20761EE /* LoadableImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableImage.swift; sourceTree = "<group>"; };
C39E32F0B876B962E418B5C2 /* DeferredFulfillment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredFulfillment.swift; sourceTree = "<group>"; };
C4756240773D26AB74C22668 /* OrientationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrientationManagerProtocol.swift; sourceTree = "<group>"; }; C4756240773D26AB74C22668 /* OrientationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrientationManagerProtocol.swift; sourceTree = "<group>"; };
C4C1C19A4BE46EDE1411ECCE /* ThreadTimelineScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadTimelineScreenViewModelProtocol.swift; sourceTree = "<group>"; }; C4C1C19A4BE46EDE1411ECCE /* ThreadTimelineScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadTimelineScreenViewModelProtocol.swift; sourceTree = "<group>"; };
C4C89820BB2B88D4EA28131C /* BugReportScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenViewModelProtocol.swift; sourceTree = "<group>"; }; C4C89820BB2B88D4EA28131C /* BugReportScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenViewModelProtocol.swift; sourceTree = "<group>"; };
@@ -2665,7 +2654,6 @@
CA89A2DD51B6BBE1DA55E263 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; }; CA89A2DD51B6BBE1DA55E263 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
CA90BD288E5AE6BC643AFDDF /* TemplateScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenCoordinator.swift; sourceTree = "<group>"; }; CA90BD288E5AE6BC643AFDDF /* TemplateScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenCoordinator.swift; sourceTree = "<group>"; };
CACA846B3E3E9A521D98B178 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; }; CACA846B3E3E9A521D98B178 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
CAD9547E47C58930E2CE8306 /* CallScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenViewModelTests.swift; sourceTree = "<group>"; };
CB7B588A06911B455AC0B4C9 /* ManageRoomMemberSheetViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageRoomMemberSheetViewModelProtocol.swift; sourceTree = "<group>"; }; CB7B588A06911B455AC0B4C9 /* ManageRoomMemberSheetViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageRoomMemberSheetViewModelProtocol.swift; sourceTree = "<group>"; };
CB98BFD8E93C7FCCEDEC46F9 /* SpacesScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpacesScreenViewModel.swift; sourceTree = "<group>"; }; CB98BFD8E93C7FCCEDEC46F9 /* SpacesScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpacesScreenViewModel.swift; sourceTree = "<group>"; };
CBBCC6E74774E79B599625D0 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; }; CBBCC6E74774E79B599625D0 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
@@ -2853,7 +2841,6 @@
ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreen.swift; sourceTree = "<group>"; }; ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreen.swift; sourceTree = "<group>"; };
EDDE826EAB1BAB80C1104980 /* SpaceFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceFlowCoordinator.swift; sourceTree = "<group>"; }; EDDE826EAB1BAB80C1104980 /* SpaceFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceFlowCoordinator.swift; sourceTree = "<group>"; };
EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = "<group>"; }; EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = "<group>"; };
EE6BFF453838CF6C3982C5A3 /* RoomDirectorySearchScreenScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenScreenViewModelTests.swift; sourceTree = "<group>"; };
EEAB5662310AE73D93815134 /* JoinRoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenViewModelProtocol.swift; sourceTree = "<group>"; }; EEAB5662310AE73D93815134 /* JoinRoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenViewModelProtocol.swift; sourceTree = "<group>"; };
EF13BFD415CA84B1272E94F8 /* PINTextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PINTextFieldTests.swift; sourceTree = "<group>"; }; EF13BFD415CA84B1272E94F8 /* PINTextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PINTextFieldTests.swift; sourceTree = "<group>"; };
EF1593DD87F974F8509BB619 /* ElementAnimations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementAnimations.swift; sourceTree = "<group>"; }; EF1593DD87F974F8509BB619 /* ElementAnimations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementAnimations.swift; sourceTree = "<group>"; };
@@ -2883,7 +2870,6 @@
F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegexTests.swift; sourceTree = "<group>"; }; F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegexTests.swift; sourceTree = "<group>"; };
F320003F490B11F808ECC5E9 /* JoinedMembersBadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinedMembersBadgeView.swift; sourceTree = "<group>"; }; F320003F490B11F808ECC5E9 /* JoinedMembersBadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinedMembersBadgeView.swift; sourceTree = "<group>"; };
F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItem.swift; sourceTree = "<group>"; }; F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItem.swift; sourceTree = "<group>"; };
F36C0A6D59717193F49EA986 /* UserSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionTests.swift; sourceTree = "<group>"; };
F37FA1A5D55633E1942B153B /* CallScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenCoordinator.swift; sourceTree = "<group>"; }; F37FA1A5D55633E1942B153B /* CallScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenCoordinator.swift; sourceTree = "<group>"; };
F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = "<group>"; }; F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = "<group>"; };
F3AAC314A877DBDB6EBE1170 /* SpaceHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceHeaderView.swift; sourceTree = "<group>"; }; F3AAC314A877DBDB6EBE1170 /* SpaceHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceHeaderView.swift; sourceTree = "<group>"; };
@@ -4366,14 +4352,6 @@
path = Scripts; path = Scripts;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
53280D2292E6C9C7821773FD /* UserSession */ = {
isa = PBXGroup;
children = (
F36C0A6D59717193F49EA986 /* UserSessionTests.swift */,
);
path = UserSession;
sourceTree = "<group>";
};
5329E48968EB951235E83DAE /* SessionVerification */ = { 5329E48968EB951235E83DAE /* SessionVerification */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -4760,7 +4738,6 @@
240610DF32F3213BEC5611D7 /* BlockedUsersScreenViewModelTests.swift */, 240610DF32F3213BEC5611D7 /* BlockedUsersScreenViewModelTests.swift */,
7EECE8B331CD169790EF284F /* BugReportScreenViewModelTests.swift */, 7EECE8B331CD169790EF284F /* BugReportScreenViewModelTests.swift */,
EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */, EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */,
CAD9547E47C58930E2CE8306 /* CallScreenViewModelTests.swift */,
0328F54E0C3AAEDDF3E05D9D /* ChatsTabFlowCoordinatorTests.swift */, 0328F54E0C3AAEDDF3E05D9D /* ChatsTabFlowCoordinatorTests.swift */,
D5EA0312A6262484AA393AC9 /* CompletionSuggestionServiceTests.swift */, D5EA0312A6262484AA393AC9 /* CompletionSuggestionServiceTests.swift */,
CA29952595B804DA221A0C1D /* ComposerToolbarViewModelTests.swift */, CA29952595B804DA221A0C1D /* ComposerToolbarViewModelTests.swift */,
@@ -4769,7 +4746,6 @@
D77F75B3E9F99864048A422A /* DeactivateAccountScreenViewModelTests.swift */, D77F75B3E9F99864048A422A /* DeactivateAccountScreenViewModelTests.swift */,
2ADF12A50186B75C68017B61 /* DeclineAndBlockScreenViewModelTests.swift */, 2ADF12A50186B75C68017B61 /* DeclineAndBlockScreenViewModelTests.swift */,
DEBB74427E24AF30CDB131B7 /* DeferredFulfillmentTests.swift */, DEBB74427E24AF30CDB131B7 /* DeferredFulfillmentTests.swift */,
6D0A27607AB09784C8501B5C /* DeveloperOptionsScreenViewModelTests.swift */,
906451FB8CF27C628152BF7A /* EditRoomAddressScreenViewModelTests.swift */, 906451FB8CF27C628152BF7A /* EditRoomAddressScreenViewModelTests.swift */,
7EA2AFF6EB59FE25234D29F3 /* ElementCallServiceTests.swift */, 7EA2AFF6EB59FE25234D29F3 /* ElementCallServiceTests.swift */,
A1087DCC491CD4C027173DDA /* EmojiPickerScreenViewModelTests.swift */, A1087DCC491CD4C027173DDA /* EmojiPickerScreenViewModelTests.swift */,
@@ -4783,7 +4759,6 @@
DE5127D6EA05B2E45D0A7D59 /* JoinRoomScreenViewModelTests.swift */, DE5127D6EA05B2E45D0A7D59 /* JoinRoomScreenViewModelTests.swift */,
FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */, FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */,
C9AC2CC94FA06F728883B694 /* KnockRequestsListScreenViewModelTests.swift */, C9AC2CC94FA06F728883B694 /* KnockRequestsListScreenViewModelTests.swift */,
6E5725BC6C63604CB769145B /* LegalInformationScreenViewModelTests.swift */,
C070FD43DC6BF4E50217965A /* LocalizationTests.swift */, C070FD43DC6BF4E50217965A /* LocalizationTests.swift */,
3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */, 3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */,
5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */, 5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */,
@@ -4815,7 +4790,6 @@
8F841F219ACDFC1D3F42FEFB /* RoomChangeRolesScreenViewModelTests.swift */, 8F841F219ACDFC1D3F42FEFB /* RoomChangeRolesScreenViewModelTests.swift */,
00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */, 00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */,
166D45E1861A73B232109843 /* RoomDetailsScreenViewModelTests.swift */, 166D45E1861A73B232109843 /* RoomDetailsScreenViewModelTests.swift */,
EE6BFF453838CF6C3982C5A3 /* RoomDirectorySearchScreenScreenViewModelTests.swift */,
6AE5800184E93CD5E02C6543 /* RoomEventStringBuilderTests.swift */, 6AE5800184E93CD5E02C6543 /* RoomEventStringBuilderTests.swift */,
4FCB2126C091EEF2454B4D56 /* RoomFlowCoordinatorTests.swift */, 4FCB2126C091EEF2454B4D56 /* RoomFlowCoordinatorTests.swift */,
8AE0C9653870803E4F91F474 /* RoomListFiltersStateTests.swift */, 8AE0C9653870803E4F91F474 /* RoomListFiltersStateTests.swift */,
@@ -4831,10 +4805,7 @@
F46E441BA50705E6CEC89FE0 /* RoomSummaryProviderTests.swift */, F46E441BA50705E6CEC89FE0 /* RoomSummaryProviderTests.swift */,
046C0D3F53B0B5EF0A1F5BEA /* RoomSummaryTests.swift */, 046C0D3F53B0B5EF0A1F5BEA /* RoomSummaryTests.swift */,
B7728AA8046D460145EAC740 /* RoomTests.swift */, B7728AA8046D460145EAC740 /* RoomTests.swift */,
2E88534A39781D76487D59DF /* SecureBackupKeyBackupScreenViewModelTests.swift */,
848F69921527D31CAACB93AF /* SecureBackupLogoutConfirmationScreenViewModelTests.swift */, 848F69921527D31CAACB93AF /* SecureBackupLogoutConfirmationScreenViewModelTests.swift */,
C0FF08D0BD7D0B4B6877AB7D /* SecureBackupRecoveryKeyScreenViewModelTests.swift */,
40316EFFEAC7B206EE9A55AE /* SecureBackupScreenViewModelTests.swift */,
0315C328FF40F84276364E66 /* SecurityAndPrivacyScreenViewModelTests.swift */, 0315C328FF40F84276364E66 /* SecurityAndPrivacyScreenViewModelTests.swift */,
277C20CDD5B64510401B6D0D /* ServerConfigurationScreenViewStateTests.swift */, 277C20CDD5B64510401B6D0D /* ServerConfigurationScreenViewStateTests.swift */,
F08776C48FFB47CACF64ED10 /* ServerConfirmationScreenViewModelTests.swift */, F08776C48FFB47CACF64ED10 /* ServerConfirmationScreenViewModelTests.swift */,
@@ -4866,7 +4837,6 @@
283974987DA7EC61D2AB57D9 /* VoiceMessageCacheTests.swift */, 283974987DA7EC61D2AB57D9 /* VoiceMessageCacheTests.swift */,
AC4F10BDD56FA77FEC742333 /* VoiceMessageMediaManagerTests.swift */, AC4F10BDD56FA77FEC742333 /* VoiceMessageMediaManagerTests.swift */,
D93C94C30E3135BC9290DE13 /* VoiceMessageRecorderTests.swift */, D93C94C30E3135BC9290DE13 /* VoiceMessageRecorderTests.swift */,
53280D2292E6C9C7821773FD /* UserSession */,
9613851C68D8C01EABFB3569 /* AppLock */, 9613851C68D8C01EABFB3569 /* AppLock */,
A6AA0A048CAE428A5CA4CBBB /* LayoutTests */, A6AA0A048CAE428A5CA4CBBB /* LayoutTests */,
7583EAC171059A86B767209F /* MediaProvider */, 7583EAC171059A86B767209F /* MediaProvider */,
@@ -6020,6 +5990,7 @@
AE52983FAFB4E0998C00EE8A /* CancellableTask.swift */, AE52983FAFB4E0998C00EE8A /* CancellableTask.swift */,
127A57D053CE8C87B5EFB089 /* Consumable.swift */, 127A57D053CE8C87B5EFB089 /* Consumable.swift */,
127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */, 127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */,
C39E32F0B876B962E418B5C2 /* DeferredFulfillment.swift */,
7B25F959A434BB9923A3223F /* ExpiringTaskRunner.swift */, 7B25F959A434BB9923A3223F /* ExpiringTaskRunner.swift */,
6A580295A56B55A856CC4084 /* InfoPlistReader.swift */, 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */,
6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */, 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */,
@@ -7635,7 +7606,6 @@
CEAEA57B7665C8E790599A78 /* BlockedUsersScreenViewModelTests.swift in Sources */, CEAEA57B7665C8E790599A78 /* BlockedUsersScreenViewModelTests.swift in Sources */,
1B2F9F368619FFF8C63C87CC /* BugReportScreenViewModelTests.swift in Sources */, 1B2F9F368619FFF8C63C87CC /* BugReportScreenViewModelTests.swift in Sources */,
7F61F9ACD5EC9E845EF3EFBF /* BugReportServiceTests.swift in Sources */, 7F61F9ACD5EC9E845EF3EFBF /* BugReportServiceTests.swift in Sources */,
366D5BFE52CB79E804C7D095 /* CallScreenViewModelTests.swift in Sources */,
4BD5AB54A6982CF19F5CC7C4 /* ChatsTabFlowCoordinatorTests.swift in Sources */, 4BD5AB54A6982CF19F5CC7C4 /* ChatsTabFlowCoordinatorTests.swift in Sources */,
B5321A1F5B26A0F3EC54909E /* CollapsibleFlowLayoutTests.swift in Sources */, B5321A1F5B26A0F3EC54909E /* CollapsibleFlowLayoutTests.swift in Sources */,
3A164187907DA43B7858F9EC /* CompletionSuggestionServiceTests.swift in Sources */, 3A164187907DA43B7858F9EC /* CompletionSuggestionServiceTests.swift in Sources */,
@@ -7645,7 +7615,6 @@
80F6C8EFCA4564B67F0D34B0 /* DeactivateAccountScreenViewModelTests.swift in Sources */, 80F6C8EFCA4564B67F0D34B0 /* DeactivateAccountScreenViewModelTests.swift in Sources */,
34390DAE0C574DAD30CCA7D9 /* DeclineAndBlockScreenViewModelTests.swift in Sources */, 34390DAE0C574DAD30CCA7D9 /* DeclineAndBlockScreenViewModelTests.swift in Sources */,
A583B70939707197B0B21DFC /* DeferredFulfillmentTests.swift in Sources */, A583B70939707197B0B21DFC /* DeferredFulfillmentTests.swift in Sources */,
864C69CF951BF36D25BE0C03 /* DeveloperOptionsScreenViewModelTests.swift in Sources */,
EDB6915EC953BB2A44AA608E /* EditRoomAddressScreenViewModelTests.swift in Sources */, EDB6915EC953BB2A44AA608E /* EditRoomAddressScreenViewModelTests.swift in Sources */,
D820B3C223E4C2E77BB2A2BF /* ElementCallServiceTests.swift in Sources */, D820B3C223E4C2E77BB2A2BF /* ElementCallServiceTests.swift in Sources */,
7AE25D29734267271106D732 /* EmojiPickerScreenViewModelTests.swift in Sources */, 7AE25D29734267271106D732 /* EmojiPickerScreenViewModelTests.swift in Sources */,
@@ -7661,7 +7630,6 @@
EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */, EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */,
BA48D6AFF6421D199148C0A1 /* KnockRequestsListScreenViewModelTests.swift in Sources */, BA48D6AFF6421D199148C0A1 /* KnockRequestsListScreenViewModelTests.swift in Sources */,
CC961529F9F1854BEC3272C9 /* LayoutMocks.swift in Sources */, CC961529F9F1854BEC3272C9 /* LayoutMocks.swift in Sources */,
8AC256AF0EC54658321C9241 /* LegalInformationScreenViewModelTests.swift in Sources */,
0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */, 0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */,
149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */, 149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */,
7434A7F02D587A920B376A9A /* LoginScreenViewModelTests.swift in Sources */, 7434A7F02D587A920B376A9A /* LoginScreenViewModelTests.swift in Sources */,
@@ -7700,7 +7668,6 @@
D2825E013A8ECFB66D9A1DE6 /* RoomChangeRolesScreenViewModelTests.swift in Sources */, D2825E013A8ECFB66D9A1DE6 /* RoomChangeRolesScreenViewModelTests.swift in Sources */,
9DD84E014ADFB2DD813022D5 /* RoomDetailsEditScreenViewModelTests.swift in Sources */, 9DD84E014ADFB2DD813022D5 /* RoomDetailsEditScreenViewModelTests.swift in Sources */,
B73E50AF1AB2EB5477E20710 /* RoomDetailsScreenViewModelTests.swift in Sources */, B73E50AF1AB2EB5477E20710 /* RoomDetailsScreenViewModelTests.swift in Sources */,
5B7D24A318AFF75AD611A026 /* RoomDirectorySearchScreenScreenViewModelTests.swift in Sources */,
E591742E509A2A009BF25F9D /* RoomEventStringBuilderTests.swift in Sources */, E591742E509A2A009BF25F9D /* RoomEventStringBuilderTests.swift in Sources */,
095D3906CF2F940C2D2D17CC /* RoomFlowCoordinatorTests.swift in Sources */, 095D3906CF2F940C2D2D17CC /* RoomFlowCoordinatorTests.swift in Sources */,
4C8C0C9FC10BA73AB7780534 /* RoomListFiltersStateTests.swift in Sources */, 4C8C0C9FC10BA73AB7780534 /* RoomListFiltersStateTests.swift in Sources */,
@@ -7716,10 +7683,7 @@
6AB306367E56A6F6DFA0E2FF /* RoomSummaryProviderTests.swift in Sources */, 6AB306367E56A6F6DFA0E2FF /* RoomSummaryProviderTests.swift in Sources */,
15913A5B07118C1268A840E4 /* RoomSummaryTests.swift in Sources */, 15913A5B07118C1268A840E4 /* RoomSummaryTests.swift in Sources */,
62811275F1ED9EA55638838E /* RoomTests.swift in Sources */, 62811275F1ED9EA55638838E /* RoomTests.swift in Sources */,
7691233E3572A9173FD96CB3 /* SecureBackupKeyBackupScreenViewModelTests.swift in Sources */,
EB87DF90CF6F8D5D12404C6E /* SecureBackupLogoutConfirmationScreenViewModelTests.swift in Sources */, EB87DF90CF6F8D5D12404C6E /* SecureBackupLogoutConfirmationScreenViewModelTests.swift in Sources */,
06B31F84CE52A7A7C271267C /* SecureBackupRecoveryKeyScreenViewModelTests.swift in Sources */,
1B8E30B35BF8F541C1318F19 /* SecureBackupScreenViewModelTests.swift in Sources */,
CB9FB2BEF313072C705AC9B5 /* SecurityAndPrivacyScreenViewModelTests.swift in Sources */, CB9FB2BEF313072C705AC9B5 /* SecurityAndPrivacyScreenViewModelTests.swift in Sources */,
53A55748D5F587C9061F98BF /* ServerConfigurationScreenViewStateTests.swift in Sources */, 53A55748D5F587C9061F98BF /* ServerConfigurationScreenViewStateTests.swift in Sources */,
89658A44C9FC19B58FD1C226 /* ServerConfirmationScreenViewModelTests.swift in Sources */, 89658A44C9FC19B58FD1C226 /* ServerConfirmationScreenViewModelTests.swift in Sources */,
@@ -7752,7 +7716,6 @@
04F17DE71A50206336749BAC /* UserPreferenceTests.swift in Sources */, 04F17DE71A50206336749BAC /* UserPreferenceTests.swift in Sources */,
73F547BEB41D3DAFAAF6E0AF /* UserProfileScreenViewModelTests.swift in Sources */, 73F547BEB41D3DAFAAF6E0AF /* UserProfileScreenViewModelTests.swift in Sources */,
627139A3D79F032BA81E3A53 /* UserSessionFlowCoordinatorTests.swift in Sources */, 627139A3D79F032BA81E3A53 /* UserSessionFlowCoordinatorTests.swift in Sources */,
81A7C020CB5F6232242A8414 /* UserSessionTests.swift in Sources */,
21AFEFB8CEFE56A3811A1F5B /* VoiceMessageCacheTests.swift in Sources */, 21AFEFB8CEFE56A3811A1F5B /* VoiceMessageCacheTests.swift in Sources */,
44BDD670FF9095ACE240A3A2 /* VoiceMessageMediaManagerTests.swift in Sources */, 44BDD670FF9095ACE240A3A2 /* VoiceMessageMediaManagerTests.swift in Sources */,
A3D7110C1E75E7B4A73BE71C /* VoiceMessageRecorderTests.swift in Sources */, A3D7110C1E75E7B4A73BE71C /* VoiceMessageRecorderTests.swift in Sources */,
@@ -8036,6 +7999,7 @@
0743CF689EBDAAF1CC0B4283 /* DeclineAndBlockScreenViewModel.swift in Sources */, 0743CF689EBDAAF1CC0B4283 /* DeclineAndBlockScreenViewModel.swift in Sources */,
F7DA19B5122AD8FA8F91B753 /* DeclineAndBlockScreenViewModelProtocol.swift in Sources */, F7DA19B5122AD8FA8F91B753 /* DeclineAndBlockScreenViewModelProtocol.swift in Sources */,
EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */, EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */,
F769F921D7823C2F1CBB5047 /* DeferredFulfillment.swift in Sources */,
5780E444F405AA1304E1C23E /* DeveloperOptionsScreen.swift in Sources */, 5780E444F405AA1304E1C23E /* DeveloperOptionsScreen.swift in Sources */,
5DD85A0FE3D85AEC3C7EFE36 /* DeveloperOptionsScreenCoordinator.swift in Sources */, 5DD85A0FE3D85AEC3C7EFE36 /* DeveloperOptionsScreenCoordinator.swift in Sources */,
6BAE34CFA9821709CFE61E50 /* DeveloperOptionsScreenHook.swift in Sources */, 6BAE34CFA9821709CFE61E50 /* DeveloperOptionsScreenHook.swift in Sources */,

View File

@@ -0,0 +1,242 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2023-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Combine
struct DeferredFulfillment<T> {
let closure: () async throws -> T
@discardableResult
func fulfill() async throws -> T {
try await closure()
}
}
struct DeferredFulfillmentError: Error {
enum Kind {
case noOutput
case unexpectedFulfillment
}
let kind: Kind
let message: String?
static func noOutput(message: String?) -> Self {
.init(kind: .noOutput, message: message)
}
static func unexpectedFulfillment(message: String?) -> Self {
.init(kind: .unexpectedFulfillment, message: message)
}
}
/// Utility that assists in subscribing to a publisher and deferring the fulfilment and results until some other actions have been performed.
/// - Parameters:
/// - publisher: The publisher to wait on.
/// - timeout: A timeout after which we give up.
/// - until: callback that evaluates outputs until some condition is reached
/// - Returns: The deferred fulfilment to be executed after some actions and that returns the result of the publisher.
func deferFulfillment<P: Publisher>(_ publisher: P,
timeout: Duration = .seconds(10),
message: String? = nil,
until condition: @escaping (P.Output) -> Bool) -> DeferredFulfillment<P.Output> {
var result: Result<P.Output, Error>?
var hasFulfilled = false
let cancellable = publisher
.sink { completion in
switch completion {
case .failure(let error):
result = .failure(error)
hasFulfilled = true
case .finished:
break
}
} receiveValue: { value in
if condition(value), !hasFulfilled {
result = .success(value)
hasFulfilled = true
}
}
return DeferredFulfillment<P.Output> {
let startTime = ContinuousClock.now
while !hasFulfilled {
await Task.yield()
if ContinuousClock.now - startTime >= timeout {
break
}
}
cancellable.cancel()
guard let unwrappedResult = result else {
throw DeferredFulfillmentError.noOutput(message: message)
}
return try unwrappedResult.get()
}
}
/// Utility that assists in observing an async sequence, deferring the fulfilment and results until some condition has been met.
/// - Parameters:
/// - asyncSequence: The sequence to wait on.
/// - timeout: A timeout after which we give up.
/// - until: callback that evaluates outputs until some condition is reached
/// - Returns: The deferred fulfilment to be executed after some actions and that returns the result of the sequence.
func deferFulfillment<Value>(_ asyncSequence: any AsyncSequence<Value, Never>,
timeout: Duration = .seconds(10),
message: String? = nil,
until condition: @escaping (Value) -> Bool) -> DeferredFulfillment<Value> {
var result: Result<Value, Error>?
var hasFulfilled = false
let task = Task {
for await value in asyncSequence {
if condition(value), !hasFulfilled {
result = .success(value)
hasFulfilled = true
}
}
}
return DeferredFulfillment<Value> {
let startTime = ContinuousClock.now
while !hasFulfilled {
await Task.yield()
if ContinuousClock.now - startTime >= timeout {
break
}
}
task.cancel()
guard let unwrappedResult = result else {
throw DeferredFulfillmentError.noOutput(message: message)
}
return try unwrappedResult.get()
}
}
/// Utility that assists in subscribing to a publisher and deferring the fulfilment and results until some other actions have been performed.
/// - Parameters:
/// - publisher: The publisher to wait on.
/// - keyPath: the key path for the expected values
/// - transitionValues: the values through which the keypath needs to transition through
/// - timeout: A timeout after which we give up.
/// - Returns: The deferred fulfilment to be executed after some actions and that returns the result of the publisher.
func deferFulfillment<P: Publisher, K: KeyPath<P.Output, V>, V: Equatable>(_ publisher: P,
keyPath: K,
transitionValues: [V],
timeout: Duration = .seconds(10)) -> DeferredFulfillment<P.Output> {
var expectedOrder = transitionValues
return deferFulfillment(publisher, timeout: timeout) { value in
let receivedValue = value[keyPath: keyPath]
if let index = expectedOrder.firstIndex(where: { $0 == receivedValue }), index == 0 {
expectedOrder.remove(at: index)
}
return expectedOrder.isEmpty
}
}
/// Utility that assists in subscribing to an async sequence and deferring the fulfilment and results until some other actions have been performed.
/// - Parameters:
/// - asyncSequence: The sequence to wait on.
/// - transitionValues: the values through which the sequence needs to transition through
/// - timeout: A timeout after which we give up.
/// - Returns: The deferred fulfilment to be executed after some actions and that returns the result of the sequence.
func deferFulfillment<Value: Equatable>(_ asyncSequence: any AsyncSequence<Value, Never>,
transitionValues: [Value],
timeout: Duration = .seconds(10)) -> DeferredFulfillment<Value> {
var expectedOrder = transitionValues
return deferFulfillment(asyncSequence, timeout: timeout) { value in
if let index = expectedOrder.firstIndex(where: { $0 == value }), index == 0 {
expectedOrder.remove(at: index)
}
return expectedOrder.isEmpty
}
}
/// Utility that assists in subscribing to a publisher and deferring the failure for a particular value until some other actions have been performed.
/// - Parameters:
/// - publisher: The publisher to wait on.
/// - timeout: A timeout after which we give up.
/// - until: callback that evaluates outputs until some condition is reached
/// - Returns: The deferred fulfilment to be executed after some actions. The publisher's result is not returned from this fulfilment.
func deferFailure<P: Publisher>(_ publisher: P,
timeout: Duration,
message: String? = nil,
until condition: @escaping (P.Output) -> Bool) -> DeferredFulfillment<Void> where P.Failure == Never {
var hasFulfilled = false
let cancellable = publisher
.sink { value in
if condition(value), !hasFulfilled {
hasFulfilled = true
}
}
return DeferredFulfillment<Void> {
let startTime = ContinuousClock.now
while !hasFulfilled {
await Task.yield()
if ContinuousClock.now - startTime >= timeout {
break
}
}
cancellable.cancel()
// For deferFailure, if hasFulfilled is true, it means the condition was met (which is a failure)
if hasFulfilled {
throw DeferredFulfillmentError.unexpectedFulfillment(message: message)
}
}
}
/// Utility that assists in subscribing to an async sequence and deferring the failure for a particular value until some other actions have been performed.
/// - Parameters:
/// - asyncSequence: The sequence to wait on.
/// - timeout: A timeout after which we give up.
/// - until: callback that evaluates outputs until some condition is reached
/// - Returns: The deferred fulfilment to be executed after some actions. The sequence's result is not returned from this fulfilment.
func deferFailure<Value>(_ asyncSequence: any AsyncSequence<Value, Never>,
timeout: Duration,
message: String? = nil,
until condition: @escaping (Value) -> Bool) -> DeferredFulfillment<Void> {
var hasFulfilled = false
let task = Task {
for await value in asyncSequence {
if condition(value), !hasFulfilled {
hasFulfilled = true
}
}
}
return DeferredFulfillment<Void> {
let startTime = ContinuousClock.now
while !hasFulfilled {
await Task.yield()
if ContinuousClock.now - startTime >= timeout {
break
}
}
task.cancel()
// For deferFailure, if hasFulfilled is true, it means the condition was met (which is a failure)
if hasFulfilled {
throw DeferredFulfillmentError.unexpectedFulfillment(message: message)
}
}
}

View File

@@ -8,33 +8,26 @@
import AVKit import AVKit
@testable import ElementX @testable import ElementX
import XCTest import Testing
final class AVMetadataMachineReadableCodeObjectExtensionsTest: XCTestCase { @Suite
func testDecodeQRCodeVersion8() { struct AVMetadataMachineReadableCodeObjectExtensionsTest {
@Test
func decodeQRCodeVersion8() throws {
// swiftlint:disable:next line_length // swiftlint:disable:next line_length
let rawDataHexString = "4a34d415452495802048bf94b094096e57d3ea43545604cf59b1704879d295cf7fdd99c62df7866da36005668747470733a2f2f73796e617073652d6f6964632e656c656d656e742e6465762f5f73796e617073652f636c69656e742f72656e64657a766f75732f3031485a32394d345936374a4e315658505759464e355a363638002168747470733a2f2f73796e617073652d6f6964632e656c656d656e742e6465762f0ec11ec11ec11ec11ec11ec11ec11ec11ec11ec11ec11ec11ec11ec11ec" let rawDataHexString = "4a34d415452495802048bf94b094096e57d3ea43545604cf59b1704879d295cf7fdd99c62df7866da36005668747470733a2f2f73796e617073652d6f6964632e656c656d656e742e6465762f5f73796e617073652f636c69656e742f72656e64657a766f75732f3031485a32394d345936374a4e315658505759464e355a363638002168747470733a2f2f73796e617073652d6f6964632e656c656d656e742e6465762f0ec11ec11ec11ec11ec11ec11ec11ec11ec11ec11ec11ec11ec11ec11ec"
// swiftlint:disable:next line_length // swiftlint:disable:next line_length
let expectedDecodedString = "4d415452495802048bf94b094096e57d3ea43545604cf59b1704879d295cf7fdd99c62df7866da36005668747470733a2f2f73796e617073652d6f6964632e656c656d656e742e6465762f5f73796e617073652f636c69656e742f72656e64657a766f75732f3031485a32394d345936374a4e315658505759464e355a363638002168747470733a2f2f73796e617073652d6f6964632e656c656d656e742e6465762f" let expectedDecodedString = "4d415452495802048bf94b094096e57d3ea43545604cf59b1704879d295cf7fdd99c62df7866da36005668747470733a2f2f73796e617073652d6f6964632e656c656d656e742e6465762f5f73796e617073652f636c69656e742f72656e64657a766f75732f3031485a32394d345936374a4e315658505759464e355a363638002168747470733a2f2f73796e617073652d6f6964632e656c656d656e742e6465762f"
let symbolVersion = 8 let symbolVersion = 8
guard let data = Data(hexString: rawDataHexString) else { let data = try #require(Data(hexString: rawDataHexString))
XCTFail("Could not initialise the raw data")
return
}
guard let resultData = try? AVMetadataMachineReadableCodeObject.removeQRProtocolData(data, symbolVersion: symbolVersion) else { let resultData = try #require(try AVMetadataMachineReadableCodeObject.removeQRProtocolData(data, symbolVersion: symbolVersion))
XCTFail("Could not remove the protocol data")
return
}
let resultString = resultData.map { String(format: "%02x", $0) }.joined() let resultString = resultData.map { String(format: "%02x", $0) }.joined()
XCTAssertEqual(expectedDecodedString, resultString) #expect(expectedDecodedString == resultString)
guard let expectedResultData = Data(hexString: expectedDecodedString) else { let expectedResultData = try #require(Data(hexString: expectedDecodedString))
XCTFail("Could not initialise the decoded data") #expect(expectedResultData == resultData)
return
}
XCTAssertEqual(expectedResultData, resultData)
} }
} }

View File

@@ -7,23 +7,16 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class AnalyticsSettingsScreenViewModelTests: XCTestCase { @Suite
final class AnalyticsSettingsScreenViewModelTests {
private var appSettings: AppSettings! private var appSettings: AppSettings!
private var viewModel: AnalyticsSettingsScreenViewModelProtocol! private var viewModel: AnalyticsSettingsScreenViewModelProtocol!
private var context: AnalyticsSettingsScreenViewModelType.Context! private var context: AnalyticsSettingsScreenViewModelType.Context!
override func setUp() { init() {
AppSettings.resetAllSettings()
}
override func tearDown() {
AppSettings.resetAllSettings()
}
@MainActor override func setUpWithError() throws {
AppSettings.resetAllSettings() AppSettings.resetAllSettings()
appSettings = AppSettings() appSettings = AppSettings()
let analyticsClient = AnalyticsClientMock() let analyticsClient = AnalyticsClientMock()
@@ -36,19 +29,26 @@ class AnalyticsSettingsScreenViewModelTests: XCTestCase {
context = viewModel.context context = viewModel.context
} }
func testInitialState() { deinit {
XCTAssertFalse(context.enableAnalytics) AppSettings.resetAllSettings()
} }
func testOptIn() { @Test
func initialState() {
#expect(!context.enableAnalytics)
}
@Test
func optIn() {
appSettings.analyticsConsentState = .optedOut appSettings.analyticsConsentState = .optedOut
context.send(viewAction: .toggleAnalytics) context.send(viewAction: .toggleAnalytics)
XCTAssertTrue(context.enableAnalytics) #expect(context.enableAnalytics)
} }
func testOptOut() { @Test
func optOut() {
appSettings.analyticsConsentState = .optedIn appSettings.analyticsConsentState = .optedIn
context.send(viewAction: .toggleAnalytics) context.send(viewAction: .toggleAnalytics)
XCTAssertFalse(context.enableAnalytics) #expect(!context.enableAnalytics)
} }
} }

View File

@@ -9,14 +9,15 @@
import AnalyticsEvents import AnalyticsEvents
@testable import ElementX @testable import ElementX
import PostHog import PostHog
import XCTest import Testing
class AnalyticsTests: XCTestCase { @Suite
private var appSettings: AppSettings! final class AnalyticsTests {
private var analyticsClient: AnalyticsClientMock! private var appSettings: AppSettings
private var posthogMock: PHGPostHogMock! private var analyticsClient: AnalyticsClientMock
private var posthogMock: PHGPostHogMock
override func setUp() { init() {
AppSettings.resetAllSettings() AppSettings.resetAllSettings()
appSettings = AppSettings() appSettings = AppSettings()
@@ -29,20 +30,22 @@ class AnalyticsTests: XCTestCase {
posthogMock.configureMockBehavior() posthogMock.configureMockBehavior()
} }
override func tearDown() { deinit {
AppSettings.resetAllSettings() AppSettings.resetAllSettings()
} }
func testAnalyticsPromptNewUser() { @Test
func analyticsPromptNewUser() {
// Given a fresh install of the app (without PostHog analytics having been set). // Given a fresh install of the app (without PostHog analytics having been set).
// When the user is prompted for analytics. // When the user is prompted for analytics.
let showPrompt = ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt let showPrompt = ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt
// Then the prompt should be shown. // 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 // Given an existing install of the app where the user previously declined PostHog
appSettings.analyticsConsentState = .optedOut appSettings.analyticsConsentState = .optedOut
@@ -50,10 +53,11 @@ class AnalyticsTests: XCTestCase {
let showPrompt = ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt let showPrompt = ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt
// Then no prompt should be shown. // 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 // Given an existing install of the app where the user previously accepted PostHog
appSettings.analyticsConsentState = .optedIn appSettings.analyticsConsentState = .optedIn
@@ -61,61 +65,67 @@ class AnalyticsTests: XCTestCase {
let showPrompt = ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt let showPrompt = ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt
// Then no prompt should be shown. // 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 // Given a fresh install of the app Analytics should be disabled
XCTAssertEqual(appSettings.analyticsConsentState, .unknown) #expect(appSettings.analyticsConsentState == .unknown)
XCTAssertFalse(ServiceLocator.shared.analytics.isEnabled) #expect(!ServiceLocator.shared.analytics.isEnabled)
XCTAssertFalse(analyticsClient.startAnalyticsConfigurationCalled) #expect(!analyticsClient.startAnalyticsConfigurationCalled)
} }
func testAnalyticsOptOut() { @Test
func analyticsOptOut() {
// Given a fresh install of the app (without PostHog analytics having been set). // Given a fresh install of the app (without PostHog analytics having been set).
// When analytics is opt-out // When analytics is opt-out
ServiceLocator.shared.analytics.optOut() ServiceLocator.shared.analytics.optOut()
// Then analytics should be disabled // Then analytics should be disabled
XCTAssertEqual(appSettings.analyticsConsentState, .optedOut) #expect(appSettings.analyticsConsentState == .optedOut)
XCTAssertFalse(ServiceLocator.shared.analytics.isEnabled) #expect(!ServiceLocator.shared.analytics.isEnabled)
XCTAssertFalse(analyticsClient.isRunning) #expect(!analyticsClient.isRunning)
// Analytics client should have been stopped // 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). // Given a fresh install of the app (without PostHog analytics having been set).
// When analytics is opt-in // When analytics is opt-in
ServiceLocator.shared.analytics.optIn() ServiceLocator.shared.analytics.optIn()
// The analytics should be enabled // The analytics should be enabled
XCTAssertEqual(appSettings.analyticsConsentState, .optedIn) #expect(appSettings.analyticsConsentState == .optedIn)
XCTAssertTrue(ServiceLocator.shared.analytics.isEnabled) #expect(ServiceLocator.shared.analytics.isEnabled)
// Analytics client should have been started // 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 // Given an existing install of the app where the user previously declined the tracking
appSettings.analyticsConsentState = .optedOut appSettings.analyticsConsentState = .optedOut
// Analytics should not start // Analytics should not start
XCTAssertFalse(ServiceLocator.shared.analytics.isEnabled) #expect(!ServiceLocator.shared.analytics.isEnabled)
ServiceLocator.shared.analytics.startIfEnabled() 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 // Given an existing install of the app where the user previously accepted the tracking
appSettings.analyticsConsentState = .optedIn appSettings.analyticsConsentState = .optedIn
// Analytics should start // Analytics should start
XCTAssertTrue(ServiceLocator.shared.analytics.isEnabled) #expect(ServiceLocator.shared.analytics.isEnabled)
ServiceLocator.shared.analytics.startIfEnabled() ServiceLocator.shared.analytics.startIfEnabled()
XCTAssertTrue(analyticsClient.startAnalyticsConfigurationCalled) #expect(analyticsClient.startAnalyticsConfigurationCalled)
} }
func testAddingUserProperties() { @Test
func addingUserProperties() {
// Given a client with no user properties set // Given a client with no user properties set
let client = PostHogAnalyticsClient() 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 // When updating the user properties
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil,
@@ -124,25 +134,26 @@ class AnalyticsTests: XCTestCase {
numSpaces: 5, recoveryState: .Disabled, verificationState: .Verified)) numSpaces: 5, recoveryState: .Disabled, verificationState: .Verified))
// Then the properties should be cached // Then the properties should be cached
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") #expect(client.pendingUserProperties != nil, "The user properties should be cached.")
XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.") #expect(client.pendingUserProperties?.ftueUseCaseSelection == .PersonalMessaging, "The use case selection should match.")
XCTAssertEqual(client.pendingUserProperties?.numFavouriteRooms, 4, "The number of favorite rooms should match.") #expect(client.pendingUserProperties?.numFavouriteRooms == 4, "The number of favorite rooms should match.")
XCTAssertEqual(client.pendingUserProperties?.numSpaces, 5, "The number of spaces should match.") #expect(client.pendingUserProperties?.numSpaces == 5, "The number of spaces should match.")
XCTAssertEqual(client.pendingUserProperties?.verificationState, AnalyticsEvent.UserProperties.VerificationState.Verified, "The verification state should match.") #expect(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?.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 // Given a client with a cached use case user properties
let client = PostHogAnalyticsClient() let client = PostHogAnalyticsClient()
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging,
numFavouriteRooms: nil, numFavouriteRooms: nil,
numSpaces: nil, recoveryState: nil, verificationState: nil)) numSpaces: nil, recoveryState: nil, verificationState: nil))
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") #expect(client.pendingUserProperties != nil, "The user properties should be cached.")
XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.") #expect(client.pendingUserProperties?.ftueUseCaseSelection == .PersonalMessaging, "The use case selection should match.")
XCTAssertNil(client.pendingUserProperties?.numFavouriteRooms, "The number of favorite rooms should not be set.") #expect(client.pendingUserProperties?.numFavouriteRooms == nil, "The number of favorite rooms should not be set.")
XCTAssertNil(client.pendingUserProperties?.numSpaces, "The number of spaces should not be set.") #expect(client.pendingUserProperties?.numSpaces == nil, "The number of spaces should not be set.")
// When updating the number of spaced // When updating the number of spaced
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: nil, client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: nil,
@@ -150,24 +161,25 @@ class AnalyticsTests: XCTestCase {
numSpaces: 5, recoveryState: nil, verificationState: nil)) numSpaces: 5, recoveryState: nil, verificationState: nil))
// Then the new properties should be updated and the existing properties should remain unchanged // Then the new properties should be updated and the existing properties should remain unchanged
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") #expect(client.pendingUserProperties != nil, "The user properties should be cached.")
XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection shouldn't have changed.") #expect(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.") #expect(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?.numSpaces == 5, "The number of spaces should have been updated.")
} }
func testSendingUserProperties() throws { @Test
func sendingUserProperties() throws {
// Given a client with user properties set // Given a client with user properties set
let client = PostHogAnalyticsClient(posthogFactory: MockPostHogFactory(mock: posthogMock)) 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, client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging,
numFavouriteRooms: nil, numFavouriteRooms: nil,
numSpaces: nil, recoveryState: nil, verificationState: nil)) numSpaces: nil, recoveryState: nil, verificationState: nil))
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") #expect(client.pendingUserProperties != nil, "The user properties should be cached.")
XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.") #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) // When sending an event (tests run under Debug configuration so this is sent to the development instance)
let someEvent = AnalyticsEvent.Error(context: nil, let someEvent = AnalyticsEvent.Error(context: nil,
@@ -186,29 +198,31 @@ class AnalyticsTests: XCTestCase {
let capturedEvent = posthogMock.capturePropertiesUserPropertiesReceivedArguments let capturedEvent = posthogMock.capturePropertiesUserPropertiesReceivedArguments
// The user properties should have been added // 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 // 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 // Given an existing install of the app where the user previously accpeted the tracking
appSettings.analyticsConsentState = .optedIn appSettings.analyticsConsentState = .optedIn
XCTAssertFalse(ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt) #expect(!ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt)
// When forgetting analytics consents // When forgetting analytics consents
ServiceLocator.shared.analytics.resetConsentState() ServiceLocator.shared.analytics.resetConsentState()
// Then the analytics prompt should be presented again // Then the analytics prompt should be presented again
XCTAssertEqual(appSettings.analyticsConsentState, .unknown) #expect(appSettings.analyticsConsentState == .unknown)
XCTAssertTrue(ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt) #expect(ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt)
} }
func testSendingAndUpdatingSuperProperties() throws { @Test
func sendingAndUpdatingSuperProperties() throws {
// Given a client with user properties set // Given a client with user properties set
let client = PostHogAnalyticsClient(posthogFactory: MockPostHogFactory(mock: posthogMock)) 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, client.updateSuperProperties(AnalyticsEvent.SuperProperties(appPlatform: .EXI,
cryptoSDK: .Rust, cryptoSDK: .Rust,
@@ -219,12 +233,12 @@ class AnalyticsTests: XCTestCase {
let screenEvent = posthogMock.screenPropertiesReceivedArguments 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 // All the super properties should have been added
XCTAssertEqual(screenEvent?.properties?["cryptoSDK"] as? String, AnalyticsEvent.SuperProperties.CryptoSDK.Rust.rawValue) #expect(screenEvent?.properties?["cryptoSDK"] as? String == AnalyticsEvent.SuperProperties.CryptoSDK.Rust.rawValue)
XCTAssertEqual(screenEvent?.properties?["appPlatform"] as? String, "EXI") #expect(screenEvent?.properties?["appPlatform"] as? String == "EXI")
XCTAssertEqual(screenEvent?.properties?["cryptoSDKVersion"] as? String, "000") #expect(screenEvent?.properties?["cryptoSDKVersion"] as? String == "000")
// It should be the same for any event // It should be the same for any event
let someEvent = AnalyticsEvent.Error(context: nil, let someEvent = AnalyticsEvent.Error(context: nil,
@@ -243,9 +257,9 @@ class AnalyticsTests: XCTestCase {
let capturedEvent = posthogMock.capturePropertiesUserPropertiesReceivedArguments let capturedEvent = posthogMock.capturePropertiesUserPropertiesReceivedArguments
// All the super properties should have been added // All the super properties should have been added
XCTAssertEqual(capturedEvent?.properties?["cryptoSDK"] as? String, AnalyticsEvent.SuperProperties.CryptoSDK.Rust.rawValue) #expect(capturedEvent?.properties?["cryptoSDK"] as? String == AnalyticsEvent.SuperProperties.CryptoSDK.Rust.rawValue)
XCTAssertEqual(capturedEvent?.properties?["appPlatform"] as? String, "EXI") #expect(capturedEvent?.properties?["appPlatform"] as? String == "EXI")
XCTAssertEqual(capturedEvent?.properties?["cryptoSDKVersion"] as? String, "000") #expect(capturedEvent?.properties?["cryptoSDKVersion"] as? String == "000")
// Updating should keep the previously set properties // Updating should keep the previously set properties
client.updateSuperProperties(AnalyticsEvent.SuperProperties(appPlatform: .EXI, client.updateSuperProperties(AnalyticsEvent.SuperProperties(appPlatform: .EXI,
@@ -256,12 +270,13 @@ class AnalyticsTests: XCTestCase {
let capturedEvent2 = posthogMock.capturePropertiesUserPropertiesReceivedArguments let capturedEvent2 = posthogMock.capturePropertiesUserPropertiesReceivedArguments
// All the super properties should have been added, with the one udpated // All the super properties should have been added, with the one udpated
XCTAssertEqual(capturedEvent2?.properties?["cryptoSDK"] as? String, AnalyticsEvent.SuperProperties.CryptoSDK.Rust.rawValue) #expect(capturedEvent2?.properties?["cryptoSDK"] as? String == AnalyticsEvent.SuperProperties.CryptoSDK.Rust.rawValue)
XCTAssertEqual(capturedEvent2?.properties?["appPlatform"] as? String, "EXI") #expect(capturedEvent2?.properties?["appPlatform"] as? String == "EXI")
XCTAssertEqual(capturedEvent2?.properties?["cryptoSDKVersion"] as? String, "001") #expect(capturedEvent2?.properties?["cryptoSDKVersion"] as? String == "001")
} }
func testShouldNotReportIfNotStarted() throws { @Test
func shouldNotReportIfNotStarted() throws {
// Given a client with user properties set // Given a client with user properties set
let client = PostHogAnalyticsClient(posthogFactory: MockPostHogFactory(mock: posthogMock)) let client = PostHogAnalyticsClient(posthogFactory: MockPostHogFactory(mock: posthogMock))
@@ -269,7 +284,7 @@ class AnalyticsTests: XCTestCase {
client.screen(AnalyticsEvent.MobileScreen(durationMs: nil, screenName: .Home)) client.screen(AnalyticsEvent.MobileScreen(durationMs: nil, screenName: .Home))
XCTAssertEqual(posthogMock.screenPropertiesCalled, false) #expect(posthogMock.screenPropertiesCalled == false)
// It should be the same for any event // It should be the same for any event
let someEvent = AnalyticsEvent.Error(context: nil, let someEvent = AnalyticsEvent.Error(context: nil,
@@ -285,13 +300,13 @@ class AnalyticsTests: XCTestCase {
wasVisibleToUser: nil) wasVisibleToUser: nil)
client.capture(someEvent) client.capture(someEvent)
XCTAssertEqual(posthogMock.capturePropertiesUserPropertiesCalled, false) #expect(posthogMock.capturePropertiesUserPropertiesCalled == false)
// start now // start now
try client.start(analyticsConfiguration: XCTUnwrap(appSettings.analyticsConfiguration)) try client.start(analyticsConfiguration: #require(appSettings.analyticsConfiguration))
XCTAssertEqual(posthogMock.optInCalled, true) #expect(posthogMock.optInCalled == true)
client.capture(someEvent) client.capture(someEvent)
XCTAssertEqual(posthogMock.capturePropertiesUserPropertiesCalled, true) #expect(posthogMock.capturePropertiesUserPropertiesCalled == true)
} }
} }

View File

@@ -7,20 +7,21 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class AppLockScreenViewModelTests: XCTestCase { @Suite
var appSettings: AppSettings! final class AppLockScreenViewModelTests {
var appLockService: AppLockService! var appSettings: AppSettings
var keychainController: KeychainControllerMock! var appLockService: AppLockService
var viewModel: AppLockScreenViewModelProtocol! var keychainController: KeychainControllerMock
var viewModel: AppLockScreenViewModelProtocol
var context: AppLockScreenViewModelType.Context { var context: AppLockScreenViewModelType.Context {
viewModel.context viewModel.context
} }
override func setUp() { init() {
AppSettings.resetAllSettings() AppSettings.resetAllSettings()
appSettings = AppSettings() appSettings = AppSettings()
keychainController = KeychainControllerMock() keychainController = KeychainControllerMock()
@@ -28,11 +29,12 @@ class AppLockScreenViewModelTests: XCTestCase {
viewModel = AppLockScreenViewModel(appLockService: appLockService) viewModel = AppLockScreenViewModel(appLockService: appLockService)
} }
override func tearDown() { deinit {
AppSettings.resetAllSettings() AppSettings.resetAllSettings()
} }
func testUnlock() async throws { @Test
func unlock() async throws {
// Given a valid PIN code. // Given a valid PIN code.
let pinCode = "2023" let pinCode = "2023"
keychainController.pinCodeReturnValue = pinCode keychainController.pinCodeReturnValue = pinCode
@@ -44,18 +46,19 @@ class AppLockScreenViewModelTests: XCTestCase {
let result = try await deferred.fulfill() let result = try await deferred.fulfill()
// The app should become unlocked. // 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. // 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. // When the user has forgotten their PIN.
context.send(viewAction: .forgotPIN) context.send(viewAction: .forgotPIN)
// Then an alert should be shown before logging out. // 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. // When confirming the logout.
let deferred = deferFulfillment(viewModel.actions) { $0 == .forceLogout } let deferred = deferFulfillment(viewModel.actions) { $0 == .forceLogout }
@@ -65,14 +68,15 @@ class AppLockScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
} }
func testUnlockFailure() async throws { @Test
func unlockFailure() async throws {
// Given an invalid PIN code. // Given an invalid PIN code.
let pinCode = "2024" let pinCode = "2024"
keychainController.pinCodeReturnValue = "2023" keychainController.pinCodeReturnValue = "2023"
keychainController.containsPINCodeBiometricStateReturnValue = false keychainController.containsPINCodeBiometricStateReturnValue = false
XCTAssertEqual(context.viewState.numberOfPINAttempts, 0, "The shouldn't be any attempts yet.") #expect(context.viewState.numberOfPINAttempts == 0, "The shouldn't be any attempts yet.")
XCTAssertFalse(context.viewState.isSubtitleWarning, "No warning should be shown yet.") #expect(!context.viewState.isSubtitleWarning, "No warning should be shown yet.")
XCTAssertNil(context.alertInfo, "No alert should be shown yet.") #expect(context.alertInfo == nil, "No alert should be shown yet.")
// When entering it on the lock screen. // When entering it on the lock screen.
var deferred = deferFulfillment(context.$viewState) { $0.numberOfPINAttempts == 1 } var deferred = deferFulfillment(context.$viewState) { $0.numberOfPINAttempts == 1 }
@@ -81,9 +85,9 @@ class AppLockScreenViewModelTests: XCTestCase {
context.send(viewAction: .clearPINCode) // Simulate the animation completion context.send(viewAction: .clearPINCode) // Simulate the animation completion
// Then a failed attempt should be shown. // Then a failed attempt should be shown.
XCTAssertEqual(context.viewState.numberOfPINAttempts, 1, "A failed attempt should have been recorded.") #expect(context.viewState.numberOfPINAttempts == 1, "A failed attempt should have been recorded.")
XCTAssertTrue(context.viewState.isSubtitleWarning, "A warning should now be shown.") #expect(context.viewState.isSubtitleWarning, "A warning should now be shown.")
XCTAssertNil(context.alertInfo, "No alert should be shown yet.") #expect(context.alertInfo == nil, "No alert should be shown yet.")
// When entering twice more // When entering twice more
deferred = deferFulfillment(context.$viewState) { $0.numberOfPINAttempts == 2 } deferred = deferFulfillment(context.$viewState) { $0.numberOfPINAttempts == 2 }
@@ -96,28 +100,28 @@ class AppLockScreenViewModelTests: XCTestCase {
context.send(viewAction: .clearPINCode) // Simulate the animation completion context.send(viewAction: .clearPINCode) // Simulate the animation completion
// Then an alert should be shown // Then an alert should be shown
XCTAssertEqual(context.viewState.numberOfPINAttempts, 3, "All the attempts should have been recorded.") #expect(context.viewState.numberOfPINAttempts == 3, "All the attempts should have been recorded.")
XCTAssertTrue(context.viewState.isSubtitleWarning, "The warning should still be shown.") #expect(context.viewState.isSubtitleWarning, "The warning should still be shown.")
XCTAssertEqual(context.alertInfo?.id, .forcedLogout, "An alert should now 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. // Given an app with a PIN set where the user attempted to unlock 3 times.
keychainController.pinCodeReturnValue = "2023" keychainController.pinCodeReturnValue = "2023"
keychainController.containsPINCodeBiometricStateReturnValue = false keychainController.containsPINCodeBiometricStateReturnValue = false
appSettings.appLockNumberOfPINAttempts = 2 appSettings.appLockNumberOfPINAttempts = 2
XCTAssertNil(context.alertInfo) #expect(context.alertInfo == nil)
let deferred = deferFulfillment(context.$viewState) { $0.numberOfPINAttempts == 3 } let deferred = deferFulfillment(context.$viewState) { $0.numberOfPINAttempts == 3 }
viewModel.context.pinCode = "0000" viewModel.context.pinCode = "0000"
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(appSettings.appLockNumberOfPINAttempts, 3, "The app should have 3 failed attempts before the force quit.") #expect(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(context.alertInfo?.id == .forcedLogout, "The app should be showing the alert before the force quit.")
// When force quitting the app and relaunching. // When force quitting the app and relaunching.
viewModel = nil
let freshViewModel = AppLockScreenViewModel(appLockService: appLockService) let freshViewModel = AppLockScreenViewModel(appLockService: appLockService)
// Then the alert should remain in place // Then the alert should remain in place
XCTAssertEqual(freshViewModel.context.alertInfo?.id, .forcedLogout, "The new view model from the fresh launch should also show the alert") #expect(freshViewModel.context.alertInfo?.id == .forcedLogout, "The new view model from the fresh launch should also show the alert")
} }
} }

View File

@@ -7,15 +7,17 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Foundation
import Testing
@MainActor @MainActor
class AppLockServiceTests: XCTestCase { @Suite
var keychainController: KeychainController! final class AppLockServiceTests {
var appSettings: AppSettings! private var keychainController: KeychainController
var service: AppLockService! private var appSettings: AppSettings
private var service: AppLockService
override func setUp() { init() {
AppSettings.resetAllSettings() AppSettings.resetAllSettings()
appSettings = AppSettings() appSettings = AppSettings()
@@ -26,34 +28,36 @@ class AppLockServiceTests: XCTestCase {
service.disable() service.disable()
} }
override func tearDown() { deinit {
AppSettings.resetAllSettings() AppSettings.resetAllSettings()
} }
// MARK: - PIN Code // MARK: - PIN Code
func testValidPINCode() { @Test
func validPINCode() {
// Given a service that hasn't been enabled. // 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. // When setting a PIN code.
let pinCode = "2023" // Highly secure PIN that is rotated every 12 months. let pinCode = "2023" // Highly secure PIN that is rotated every 12 months.
guard case .success = service.setupPINCode(pinCode) else { guard case .success = service.setupPINCode(pinCode) else {
XCTFail("The PIN should be valid.") Issue.record("The PIN should be valid.")
return return
} }
// Then service should be enabled and only the provided PIN should work to unlock the app. // 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.") #expect(service.isEnabled, "The service should become enabled when setting a PIN.")
XCTAssertTrue(service.unlock(with: pinCode), "The provided PIN code should work.") #expect(service.unlock(with: pinCode), "The provided PIN code should work.")
XCTAssertFalse(service.unlock(with: "2024"), "No other PIN code should work.") #expect(!service.unlock(with: "2024"), "No other PIN code should work.")
XCTAssertFalse(service.unlock(with: "1234"), "No other PIN code should work.") #expect(!service.unlock(with: "1234"), "No other PIN code should work.")
XCTAssertFalse(service.unlock(with: "9999"), "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. // 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. // When setting a PIN code that is in the block list.
let pinCode = appSettings.appLockPINCodeBlockList[0] let pinCode = appSettings.appLockPINCodeBlockList[0]
@@ -61,16 +65,17 @@ class AppLockServiceTests: XCTestCase {
// Then the setup should fail and the service be left as disabled. // Then the setup should fail and the service be left as disabled.
guard case let .failure(error) = result else { guard case let .failure(error) = result else {
XCTFail("The call should have failed.") Issue.record("The call should have failed.")
return return
} }
XCTAssertEqual(error, .weakPIN, "The PIN should be rejected as weak.") #expect(error == .weakPIN, "The PIN should be rejected as weak.")
XCTAssertFalse(service.isEnabled, "The service should remain disabled.") #expect(!service.isEnabled, "The service should remain disabled.")
} }
func testShortPINCode() { @Test
func shortPINCode() {
// Given a service that hasn't been enabled. // 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 // When setting a PIN code that is too short
let pinCode = "123" let pinCode = "123"
@@ -78,16 +83,17 @@ class AppLockServiceTests: XCTestCase {
// Then the setup should fail and the service be left as disabled. // Then the setup should fail and the service be left as disabled.
guard case let .failure(error) = result else { guard case let .failure(error) = result else {
XCTFail("The call should have failed.") Issue.record("The call should have failed.")
return return
} }
XCTAssertEqual(error, .invalidPIN, "The PIN should be rejected as invalid.") #expect(error == .invalidPIN, "The PIN should be rejected as invalid.")
XCTAssertFalse(service.isEnabled, "The service should remain disabled.") #expect(!service.isEnabled, "The service should remain disabled.")
} }
func testNonNumericPINCode() { @Test
func nonNumericPINCode() {
// Given a service that hasn't been enabled. // 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 // When setting a PIN code that is too short
let pinCode = "abcd" let pinCode = "abcd"
@@ -95,116 +101,121 @@ class AppLockServiceTests: XCTestCase {
// Then the setup should fail and the service be left as disabled. // Then the setup should fail and the service be left as disabled.
guard case let .failure(error) = result else { guard case let .failure(error) = result else {
XCTFail("The call should have failed.") Issue.record("The call should have failed.")
return return
} }
XCTAssertEqual(error, .invalidPIN, "The PIN should be rejected as invalid.") #expect(error == .invalidPIN, "The PIN should be rejected as invalid.")
XCTAssertFalse(service.isEnabled, "The service should remain disabled.") #expect(!service.isEnabled, "The service should remain disabled.")
} }
func testChangePINCode() { @Test
func changePINCode() {
// Given a service that is already enabled with a PIN. // Given a service that is already enabled with a PIN.
let pinCode = "2023" let pinCode = "2023"
let newPINCode = "2024" let newPINCode = "2024"
guard case .success = service.setupPINCode(pinCode) else { guard case .success = service.setupPINCode(pinCode) else {
XCTFail("The PIN should be valid.") Issue.record("The PIN should be valid.")
return return
} }
XCTAssertTrue(service.isEnabled, "The service should be enabled.") #expect(service.isEnabled, "The service should be enabled.")
XCTAssertTrue(service.unlock(with: pinCode), "The initial PIN should work.") #expect(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.unlock(with: newPINCode), "The PIN we're about to set should not work.")
// When updating the PIN code. // When updating the PIN code.
guard case .success = service.setupPINCode(newPINCode) else { guard case .success = service.setupPINCode(newPINCode) else {
XCTFail("The PIN should be valid.") Issue.record("The PIN should be valid.")
return return
} }
// Then the old code should not be accepted. // Then the old code should not be accepted.
XCTAssertTrue(service.isEnabled, "The service should remain enabled.") #expect(service.isEnabled, "The service should remain enabled.")
XCTAssertTrue(service.unlock(with: newPINCode), "The new PIN should work.") #expect(service.unlock(with: newPINCode), "The new PIN should work.")
XCTAssertFalse(service.unlock(with: pinCode), "The original PIN should be rejected.") #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. // Given a service that is already enabled with a PIN.
let pinCode = "2023" let pinCode = "2023"
let invalidPIN = appSettings.appLockPINCodeBlockList[0] let invalidPIN = appSettings.appLockPINCodeBlockList[0]
guard case .success = service.setupPINCode(pinCode) else { guard case .success = service.setupPINCode(pinCode) else {
XCTFail("The PIN should be valid.") Issue.record("The PIN should be valid.")
return return
} }
XCTAssertTrue(service.isEnabled, "The service should be enabled.") #expect(service.isEnabled, "The service should be enabled.")
XCTAssertTrue(service.unlock(with: pinCode), "The initial PIN should work.") #expect(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.unlock(with: invalidPIN), "The PIN we're about to set should not work.")
// When updating the PIN code that is in the block list. // When updating the PIN code that is in the block list.
let result = service.setupPINCode(invalidPIN) let result = service.setupPINCode(invalidPIN)
// Then it should fail and nothing should change. // Then it should fail and nothing should change.
guard case let .failure(error) = result else { guard case let .failure(error) = result else {
XCTFail("The call should have failed.") Issue.record("The call should have failed.")
return return
} }
XCTAssertEqual(error, .weakPIN, "The PIN should be rejected as weak.") #expect(error == .weakPIN, "The PIN should be rejected as weak.")
XCTAssertTrue(service.isEnabled, "The service should remain enabled.") #expect(service.isEnabled, "The service should remain enabled.")
XCTAssertFalse(service.unlock(with: invalidPIN), "The rejected PIN shouldn't work.") #expect(!service.unlock(with: invalidPIN), "The rejected PIN shouldn't work.")
XCTAssertTrue(service.unlock(with: pinCode), "The original PIN should continue to 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. // Given a service that is already enabled with a PIN.
let pinCode = "2023" let pinCode = "2023"
guard case .success = service.setupPINCode(pinCode) else { guard case .success = service.setupPINCode(pinCode) else {
XCTFail("The PIN should be valid.") Issue.record("The PIN should be valid.")
return return
} }
XCTAssertTrue(service.isEnabled, "The service should be enabled.") #expect(service.isEnabled, "The service should be enabled.")
XCTAssertTrue(service.unlock(with: pinCode), "The initial PIN should work.") #expect(service.unlock(with: pinCode), "The initial PIN should work.")
// When disabling the PIN code. // When disabling the PIN code.
service.disable() service.disable()
// Then the PIN code should be removed. // Then the PIN code should be removed.
XCTAssertFalse(service.isEnabled, "The service should no longer be enabled.") #expect(!service.isEnabled, "The service should no longer be enabled.")
XCTAssertFalse(service.unlock(with: pinCode), "The initial PIN shouldn't work any more.") #expect(!service.unlock(with: pinCode), "The initial PIN shouldn't work any more.")
} }
// MARK: - Biometric Unlock // MARK: - Biometric Unlock
func testEnableBiometricUnlock() async { @Test
func enableBiometricUnlock() async {
// Given a service with the PIN code already set. // Given a service with the PIN code already set.
let context = LAContextMock() let context = LAContextMock()
context.biometryTypeValue = .touchID context.biometryTypeValue = .touchID
context.evaluatedPolicyDomainStateValue = Data("👆".utf8) context.evaluatedPolicyDomainStateValue = Data("👆".utf8)
service = AppLockService(keychainController: keychainController, appSettings: appSettings, context: context) service = AppLockService(keychainController: keychainController, appSettings: appSettings, context: context)
guard case .success = service.setupPINCode("2023") else { guard case .success = service.setupPINCode("2023") else {
XCTFail("The PIN should be valid.") Issue.record("The PIN should be valid.")
return return
} }
XCTAssertTrue(service.isEnabled, "The service should be enabled.") #expect(service.isEnabled, "The service should be enabled.")
XCTAssertEqual(service.biometryType, .touchID, "The biometry type should be in sync with the mock.") #expect(service.biometryType == .touchID, "The biometry type should be in sync with the mock.")
XCTAssertFalse(service.biometricUnlockEnabled, "Biometric unlock should not be enabled.") #expect(!service.biometricUnlockEnabled, "Biometric unlock should not be enabled.")
XCTAssertFalse(service.biometricUnlockTrusted, "Biometric unlock should not be trusted.") #expect(!service.biometricUnlockTrusted, "Biometric unlock should not be trusted.")
// When enabling biometric unlock. // When enabling biometric unlock.
guard case .success = service.enableBiometricUnlock() else { guard case .success = service.enableBiometricUnlock() else {
XCTFail("The biometric lock should enable.") Issue.record("The biometric lock should enable.")
return return
} }
context.evaluatePolicyReturnValue = true context.evaluatePolicyReturnValue = true
// Then the service should be unlockable with biometrics. // Then the service should be unlockable with biometrics.
XCTAssertEqual(service.biometryType, .touchID, "The biometry type should not change.") #expect(service.biometryType == .touchID, "The biometry type should not change.")
XCTAssertTrue(service.biometricUnlockEnabled, "Biometric unlock should now be enabled.") #expect(service.biometricUnlockEnabled, "Biometric unlock should now be enabled.")
XCTAssertTrue(service.biometricUnlockTrusted, "Biometric unlock should now be trusted.") #expect(service.biometricUnlockTrusted, "Biometric unlock should now be trusted.")
guard await service.unlockWithBiometrics() == .unlocked else { guard await service.unlockWithBiometrics() == .unlocked else {
XCTFail("The biometric unlock should work.") Issue.record("The biometric unlock should work.")
return return
} }
} }
func testBiometricUnlockTrust() { @Test
func biometricUnlockTrust() {
// Given a service with the PIN code already set. // Given a service with the PIN code already set.
let context = LAContextMock() let context = LAContextMock()
context.biometryTypeValue = .touchID context.biometryTypeValue = .touchID
@@ -212,129 +223,133 @@ class AppLockServiceTests: XCTestCase {
service = AppLockService(keychainController: keychainController, appSettings: appSettings, context: context) service = AppLockService(keychainController: keychainController, appSettings: appSettings, context: context)
let pinCode = "2023" let pinCode = "2023"
guard case .success = service.setupPINCode(pinCode) else { guard case .success = service.setupPINCode(pinCode) else {
XCTFail("The PIN should be valid.") Issue.record("The PIN should be valid.")
return return
} }
guard case .success = service.enableBiometricUnlock() else { guard case .success = service.enableBiometricUnlock() else {
XCTFail("The biometric lock should enable.") Issue.record("The biometric lock should enable.")
return return
} }
XCTAssertTrue(service.isEnabled, "The service should be enabled.") #expect(service.isEnabled, "The service should be enabled.")
XCTAssertEqual(service.biometryType, .touchID, "The biometry type should be in sync with the mock.") #expect(service.biometryType == .touchID, "The biometry type should be in sync with the mock.")
XCTAssertTrue(service.biometricUnlockEnabled, "Biometric unlock should be enabled.") #expect(service.biometricUnlockEnabled, "Biometric unlock should be enabled.")
XCTAssertTrue(service.biometricUnlockTrusted, "Biometric unlock should be trusted.") #expect(service.biometricUnlockTrusted, "Biometric unlock should be trusted.")
// When the user changes biometric data. // When the user changes biometric data.
context.evaluatedPolicyDomainStateValue = Data("👈".utf8) context.evaluatedPolicyDomainStateValue = Data("👈".utf8)
// Then biometric lock should remain enabled but untrusted. // Then biometric lock should remain enabled but untrusted.
XCTAssertTrue(service.isEnabled, "The service should remain enabled.") #expect(service.isEnabled, "The service should remain enabled.")
XCTAssertEqual(service.biometryType, .touchID, "The biometry type should not change.") #expect(service.biometryType == .touchID, "The biometry type should not change.")
XCTAssertTrue(service.biometricUnlockEnabled, "Biometric unlock should remain enabled.") #expect(service.biometricUnlockEnabled, "Biometric unlock should remain enabled.")
XCTAssertFalse(service.biometricUnlockTrusted, "Biometric unlock should no longer be trusted.") #expect(!service.biometricUnlockTrusted, "Biometric unlock should no longer be trusted.")
// When the user confirms their PIN code. // 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. // Then the biometric lock should once again be trusted.
XCTAssertTrue(service.isEnabled, "The service should remain enabled.") #expect(service.isEnabled, "The service should remain enabled.")
XCTAssertEqual(service.biometryType, .touchID, "The biometry type should not change.") #expect(service.biometryType == .touchID, "The biometry type should not change.")
XCTAssertTrue(service.biometricUnlockEnabled, "Biometric unlock should remain enabled.") #expect(service.biometricUnlockEnabled, "Biometric unlock should remain enabled.")
XCTAssertTrue(service.biometricUnlockTrusted, "Biometric unlock should once again be trusted.") #expect(service.biometricUnlockTrusted, "Biometric unlock should once again be trusted.")
} }
func testDisableBiometricUnlock() { @Test
func disableBiometricUnlock() {
// Given a service with the PIN code already set. // Given a service with the PIN code already set.
let context = LAContextMock() let context = LAContextMock()
context.biometryTypeValue = .touchID context.biometryTypeValue = .touchID
context.evaluatedPolicyDomainStateValue = Data("👆".utf8) context.evaluatedPolicyDomainStateValue = Data("👆".utf8)
service = AppLockService(keychainController: keychainController, appSettings: appSettings, context: context) service = AppLockService(keychainController: keychainController, appSettings: appSettings, context: context)
guard case .success = service.setupPINCode("2023") else { guard case .success = service.setupPINCode("2023") else {
XCTFail("The PIN should be valid.") Issue.record("The PIN should be valid.")
return return
} }
guard case .success = service.enableBiometricUnlock() else { guard case .success = service.enableBiometricUnlock() else {
XCTFail("The biometric lock should enable.") Issue.record("The biometric lock should enable.")
return return
} }
XCTAssertTrue(service.isEnabled, "The service should be enabled.") #expect(service.isEnabled, "The service should be enabled.")
XCTAssertEqual(service.biometryType, .touchID, "The biometry type should be in sync with the mock.") #expect(service.biometryType == .touchID, "The biometry type should be in sync with the mock.")
XCTAssertTrue(service.biometricUnlockEnabled, "Biometric unlock should be enabled.") #expect(service.biometricUnlockEnabled, "Biometric unlock should be enabled.")
XCTAssertTrue(service.biometricUnlockTrusted, "Biometric unlock should be trusted.") #expect(service.biometricUnlockTrusted, "Biometric unlock should be trusted.")
// When disabling biometric unlock. // When disabling biometric unlock.
service.disableBiometricUnlock() service.disableBiometricUnlock()
// Then only PIN unlock should remain enabled. // Then only PIN unlock should remain enabled.
XCTAssertTrue(service.isEnabled, "The service should remain enabled.") #expect(service.isEnabled, "The service should remain enabled.")
XCTAssertEqual(service.biometryType, .touchID, "The biometry type should not change.") #expect(service.biometryType == .touchID, "The biometry type should not change.")
XCTAssertFalse(service.biometricUnlockEnabled, "Biometric unlock should become disabled.") #expect(!service.biometricUnlockEnabled, "Biometric unlock should become disabled.")
XCTAssertFalse(service.biometricUnlockTrusted, "Biometric unlock should no longer be trusted.") #expect(!service.biometricUnlockTrusted, "Biometric unlock should no longer be trusted.")
} }
func testDisablePINWithBiometricUnlock() { @Test
func disablePINWithBiometricUnlock() {
// Given a service with the PIN code already set. // Given a service with the PIN code already set.
let context = LAContextMock() let context = LAContextMock()
context.biometryTypeValue = .touchID context.biometryTypeValue = .touchID
context.evaluatedPolicyDomainStateValue = Data("👆".utf8) context.evaluatedPolicyDomainStateValue = Data("👆".utf8)
service = AppLockService(keychainController: keychainController, appSettings: appSettings, context: context) service = AppLockService(keychainController: keychainController, appSettings: appSettings, context: context)
guard case .success = service.setupPINCode("2023") else { guard case .success = service.setupPINCode("2023") else {
XCTFail("The PIN should be valid.") Issue.record("The PIN should be valid.")
return return
} }
guard case .success = service.enableBiometricUnlock() else { guard case .success = service.enableBiometricUnlock() else {
XCTFail("The biometric lock should enable.") Issue.record("The biometric lock should enable.")
return return
} }
XCTAssertTrue(service.isEnabled, "The service should be enabled.") #expect(service.isEnabled, "The service should be enabled.")
XCTAssertTrue(service.biometricUnlockEnabled, "Biometric unlock should be enabled.") #expect(service.biometricUnlockEnabled, "Biometric unlock should be enabled.")
XCTAssertTrue(service.biometricUnlockTrusted, "Biometric unlock should be trusted.") #expect(service.biometricUnlockTrusted, "Biometric unlock should be trusted.")
// When disabling the PIN lock. // When disabling the PIN lock.
service.disable() service.disable()
// Then both PIN and biometric unlock should be disabled. // Then both PIN and biometric unlock should be disabled.
XCTAssertFalse(service.isEnabled, "The service should remain enabled.") #expect(!service.isEnabled, "The service should remain enabled.")
XCTAssertFalse(service.biometricUnlockEnabled, "Biometric unlock should become disabled.") #expect(!service.biometricUnlockEnabled, "Biometric unlock should become disabled.")
XCTAssertFalse(service.biometricUnlockTrusted, "Biometric unlock should no longer be trusted.") #expect(!service.biometricUnlockTrusted, "Biometric unlock should no longer be trusted.")
} }
// MARK: - Attempt failures // MARK: - Attempt failures
func testResetAttemptsOnUnlock() { @Test
func resetAttemptsOnUnlock() {
// Given a service that is enabled and has failed unlock attempts. // Given a service that is enabled and has failed unlock attempts.
let pinCode = "2023" let pinCode = "2023"
guard case .success = service.setupPINCode(pinCode) else { guard case .success = service.setupPINCode(pinCode) else {
XCTFail("The PIN should be valid.") Issue.record("The PIN should be valid.")
return return
} }
appSettings.appLockNumberOfPINAttempts = 2 appSettings.appLockNumberOfPINAttempts = 2
XCTAssertEqual(appSettings.appLockNumberOfPINAttempts, 2, "The initial conditions should be stored.") #expect(appSettings.appLockNumberOfPINAttempts == 2, "The initial conditions should be stored.")
XCTAssertTrue(service.isEnabled, "The service should be enabled.") #expect(service.isEnabled, "The service should be enabled.")
// When unlocking the service // 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. // 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. // Given a service that is enabled and has failed unlock attempts.
let pinCode = "2023" let pinCode = "2023"
guard case .success = service.setupPINCode(pinCode) else { guard case .success = service.setupPINCode(pinCode) else {
XCTFail("The PIN should be valid.") Issue.record("The PIN should be valid.")
return return
} }
appSettings.appLockNumberOfPINAttempts = 2 appSettings.appLockNumberOfPINAttempts = 2
XCTAssertEqual(appSettings.appLockNumberOfPINAttempts, 2, "The initial conditions should be stored.") #expect(appSettings.appLockNumberOfPINAttempts == 2, "The initial conditions should be stored.")
XCTAssertTrue(service.isEnabled, "The service should be enabled.") #expect(service.isEnabled, "The service should be enabled.")
// When disabling the service // When disabling the service
service.disable() 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. // Then the attempts counts should both be reset.
XCTAssertEqual(appSettings.appLockNumberOfPINAttempts, 0, "The PIN attempts should be reset.") #expect(appSettings.appLockNumberOfPINAttempts == 0, "The PIN attempts should be reset.")
} }
} }

View File

@@ -7,39 +7,40 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class AppLockSetupSettingsScreenViewModelTests: XCTestCase { @Suite
var appLockService: AppLockServiceProtocol! struct AppLockSetupSettingsScreenViewModelTests {
var keychainController: KeychainControllerMock! var appLockService: AppLockServiceProtocol
var viewModel: AppLockSetupSettingsScreenViewModelProtocol! var keychainController: KeychainControllerMock
var viewModel: AppLockSetupSettingsScreenViewModelProtocol
var context: AppLockSetupSettingsScreenViewModelType.Context { var context: AppLockSetupSettingsScreenViewModelType.Context {
viewModel.context viewModel.context
} }
override func setUpWithError() throws { init() {
keychainController = KeychainControllerMock() keychainController = KeychainControllerMock()
appLockService = AppLockService(keychainController: keychainController, appSettings: AppSettings()) appLockService = AppLockService(keychainController: keychainController, appSettings: AppSettings())
viewModel = AppLockSetupSettingsScreenViewModel(appLockService: AppLockServiceMock.mock()) viewModel = AppLockSetupSettingsScreenViewModel(appLockService: AppLockServiceMock.mock())
} }
func testDisablingShowsAlert() { @Test
func disablingShowsAlert() {
// Given a fresh screen with the PIN code enabled. // Given a fresh screen with the PIN code enabled.
let pinCode = "2023" let pinCode = "2023"
keychainController.pinCodeReturnValue = pinCode keychainController.pinCodeReturnValue = pinCode
keychainController.containsPINCodeReturnValue = true keychainController.containsPINCodeReturnValue = true
XCTAssertNil(context.alertInfo) #expect(context.alertInfo == nil)
XCTAssertTrue(appLockService.isEnabled) #expect(appLockService.isEnabled)
// When disabling the PIN code lock. // When disabling the PIN code lock.
context.send(viewAction: .disable) context.send(viewAction: .disable)
// Then an alert should be shown before disabling it. // Then an alert should be shown before disabling it.
XCTAssertNotNil(context.alertInfo) #expect(context.alertInfo != nil)
XCTAssertTrue(appLockService.isEnabled) #expect(appLockService.isEnabled)
} }
} }

View File

@@ -7,18 +7,19 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class AppLockSetupBiometricsScreenViewModelTests: XCTestCase { @Suite
var appLockService: AppLockServiceMock! final class AppLockSetupBiometricsScreenViewModelTests {
var viewModel: AppLockSetupBiometricsScreenViewModelProtocol! var appLockService: AppLockServiceMock
var viewModel: AppLockSetupBiometricsScreenViewModelProtocol
var context: AppLockSetupBiometricsScreenViewModelType.Context { var context: AppLockSetupBiometricsScreenViewModelType.Context {
viewModel.context viewModel.context
} }
override func setUp() { init() {
AppSettings.resetAllSettings() AppSettings.resetAllSettings()
appLockService = AppLockServiceMock() appLockService = AppLockServiceMock()
@@ -28,27 +29,29 @@ class AppLockSetupBiometricsScreenViewModelTests: XCTestCase {
viewModel = AppLockSetupBiometricsScreenViewModel(appLockService: appLockService) viewModel = AppLockSetupBiometricsScreenViewModel(appLockService: appLockService)
} }
override func tearDown() { deinit {
AppSettings.resetAllSettings() AppSettings.resetAllSettings()
} }
func testAllow() async throws { @Test
func allow() async throws {
// When allowing Touch/Face ID. // When allowing Touch/Face ID.
let deferred = deferFulfillment(viewModel.actions) { $0 == .continue } let deferred = deferFulfillment(viewModel.actions) { $0 == .continue }
context.send(viewAction: .allow) context.send(viewAction: .allow)
try await deferred.fulfill() try await deferred.fulfill()
// Then the service should now have biometric unlock enabled. // 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. // When skipping biometrics.
let deferred = deferFulfillment(viewModel.actions) { $0 == .continue } let deferred = deferFulfillment(viewModel.actions) { $0 == .continue }
context.send(viewAction: .skip) context.send(viewAction: .skip)
try await deferred.fulfill() try await deferred.fulfill()
// Then the service should now have biometric unlock enabled. // Then the service should now have biometric unlock enabled.
XCTAssertEqual(appLockService.enableBiometricUnlockCallsCount, 0) #expect(appLockService.enableBiometricUnlockCallsCount == 0)
} }
} }

View File

@@ -7,10 +7,11 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class AppLockSetupPINScreenViewModelTests: XCTestCase { @Suite
final class AppLockSetupPINScreenViewModelTests {
var appLockService: AppLockService! var appLockService: AppLockService!
var keychainController: KeychainControllerMock! var keychainController: KeychainControllerMock!
var viewModel: AppLockSetupPINScreenViewModelProtocol! var viewModel: AppLockSetupPINScreenViewModelProtocol!
@@ -19,42 +20,40 @@ class AppLockSetupPINScreenViewModelTests: XCTestCase {
viewModel.context viewModel.context
} }
override func setUp() { deinit {
AppSettings.resetAllSettings()
keychainController = KeychainControllerMock()
appLockService = AppLockService(keychainController: keychainController, appSettings: AppSettings())
}
override func tearDown() {
AppSettings.resetAllSettings() AppSettings.resetAllSettings()
} }
func testCreatePIN() async throws { @Test
func createPIN() async throws {
setup(mode: .create)
// Given the screen in create mode. // Given the screen in create mode.
viewModel = AppLockSetupPINScreenViewModel(initialMode: .create, isMandatory: false, appLockService: appLockService) #expect(context.viewState.mode == .create, "The mode should start as creation.")
XCTAssertEqual(context.viewState.mode, .create, "The mode should start as creation.")
// When entering an new PIN. // 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" context.pinCode = "2023"
try await createDeferred.fulfill() try await createDeferred.fulfill()
// Then the screen should transition to the confirm mode. // 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. // 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" context.pinCode = "2023"
// Then the screen should signal it is complete. // Then the screen should signal it is complete.
try await confirmDeferred.fulfill() try await confirmDeferred.fulfill()
} }
func testCreateWeakPIN() async throws { @Test
func createWeakPIN() async throws {
setup(mode: .create)
// Given the screen in create mode. // Given the screen in create mode.
viewModel = AppLockSetupPINScreenViewModel(initialMode: .create, isMandatory: false, appLockService: appLockService) #expect(context.viewState.mode == .create, "The mode should start as creation.")
XCTAssertEqual(context.viewState.mode, .create, "The mode should start as creation.") #expect(context.alertInfo == nil, "There shouldn't be an alert to begin with.")
XCTAssertNil(context.alertInfo, "There shouldn't be an alert to begin with.")
// When entering a weak PIN on the blocklist. // When entering a weak PIN on the blocklist.
let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil } let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
@@ -62,22 +61,24 @@ class AppLockSetupPINScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
// Then the PIN should be rejected and the user alerted. // Then the PIN should be rejected and the user alerted.
XCTAssertEqual(context.alertInfo?.id, .weakPIN, "The weak PIN should be rejected.") #expect(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.viewState.mode == .create, "The mode shouldn't transition after an invalid PIN code.")
} }
func testCreatePINMismatch() async throws { @Test
// Given the confirm mode after entering a new PIN. func createPINMismatch() async throws {
viewModel = AppLockSetupPINScreenViewModel(initialMode: .create, isMandatory: false, appLockService: appLockService) setup(mode: .create)
XCTAssertEqual(context.viewState.mode, .create, "The mode should start as creation.")
XCTAssertNil(context.alertInfo, "There shouldn't be an alert to begin with.")
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" context.pinCode = "2023"
try await createDeferred.fulfill() try await createDeferred.fulfill()
XCTAssertEqual(context.viewState.mode, .confirm, "The mode should transition to confirmation.") #expect(context.viewState.mode == .confirm, "The mode should transition to confirmation.")
XCTAssertEqual(context.viewState.numberOfConfirmAttempts, 0, "The mode should start with zero attempts.") #expect(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.alertInfo == nil, "There shouldn't be an alert after a valid initial PIN.")
// When entering the new PIN incorrectly // When entering the new PIN incorrectly
var deferred = deferFulfillment(context.$viewState) { $0.numberOfConfirmAttempts == 1 } var deferred = deferFulfillment(context.$viewState) { $0.numberOfConfirmAttempts == 1 }
@@ -85,8 +86,8 @@ class AppLockSetupPINScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
// Then the user should be alerted. // Then the user should be alerted.
XCTAssertEqual(context.viewState.numberOfConfirmAttempts, 1, "The mismatch should be counted.") #expect(context.viewState.numberOfConfirmAttempts == 1, "The mismatch should be counted.")
XCTAssertEqual(context.alertInfo?.id, .pinMismatch, "A PIN mismatch should be rejected.") #expect(context.alertInfo?.id == .pinMismatch, "A PIN mismatch should be rejected.")
// When dismissing the alert and repeating twice more. // When dismissing the alert and repeating twice more.
context.alertInfo?.primaryButton.action?() context.alertInfo?.primaryButton.action?()
@@ -97,42 +98,46 @@ class AppLockSetupPINScreenViewModelTests: XCTestCase {
deferred = deferFulfillment(context.$viewState) { $0.numberOfConfirmAttempts == 3 } deferred = deferFulfillment(context.$viewState) { $0.numberOfConfirmAttempts == 3 }
context.pinCode = "2024" context.pinCode = "2024"
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(context.viewState.numberOfConfirmAttempts, 3, "All the mismatches should be counted.") #expect(context.viewState.numberOfConfirmAttempts == 3, "All the mismatches should be counted.")
XCTAssertEqual(context.alertInfo?.id, .pinMismatch, "A PIN mismatch should be rejected.") #expect(context.alertInfo?.id == .pinMismatch, "A PIN mismatch should be rejected.")
// Then tapping the alert button should reset back to create mode. // Then tapping the alert button should reset back to create mode.
context.alertInfo?.primaryButton.action?() 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. // Given the screen in unlock mode.
viewModel = AppLockSetupPINScreenViewModel(initialMode: .unlock, isMandatory: false, appLockService: appLockService)
let pinCode = "2023" let pinCode = "2023"
keychainController.pinCodeReturnValue = pinCode keychainController.pinCodeReturnValue = pinCode
keychainController.containsPINCodeReturnValue = true keychainController.containsPINCodeReturnValue = true
keychainController.containsPINCodeBiometricStateReturnValue = false keychainController.containsPINCodeBiometricStateReturnValue = false
// When entering the configured PIN. // 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 context.pinCode = pinCode
// Then the screen should signal it is complete. // Then the screen should signal it is complete.
try await deferred.fulfill() try await deferred.fulfill()
} }
func testForgotPIN() async throws { @Test
func forgotPIN() async throws {
setup(mode: .unlock)
// Given the screen in unlock mode. // Given the screen in unlock mode.
viewModel = AppLockSetupPINScreenViewModel(initialMode: .unlock, isMandatory: false, appLockService: appLockService) #expect(context.alertInfo == nil, "There shouldn't be an alert to begin with.")
XCTAssertNil(context.alertInfo, "There shouldn't be an alert to begin with.") #expect(!context.viewState.isLoggingOut, "The view should not start disabled.")
XCTAssertFalse(context.viewState.isLoggingOut, "The view should not start disabled.")
// When the user has forgotten their PIN. // When the user has forgotten their PIN.
context.send(viewAction: .forgotPIN) context.send(viewAction: .forgotPIN)
// Then an alert should be shown before logging out. // Then an alert should be shown before logging out.
XCTAssertEqual(context.alertInfo?.id, .confirmResetPIN, "The weak PIN should be rejected.") #expect(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.viewState.isLoggingOut, "The view should not be disabled until the user confirms.")
// When confirming the logout. // When confirming the logout.
let deferred = deferFulfillment(viewModel.actions) { $0 == .forceLogout } let deferred = deferFulfillment(viewModel.actions) { $0 == .forceLogout }
@@ -140,44 +145,52 @@ class AppLockSetupPINScreenViewModelTests: XCTestCase {
// Then a force logout should be initiated. // Then a force logout should be initiated.
try await deferred.fulfill() 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. // Given the screen in unlock mode.
viewModel = AppLockSetupPINScreenViewModel(initialMode: .unlock, isMandatory: false, appLockService: appLockService)
keychainController.pinCodeReturnValue = "2023" keychainController.pinCodeReturnValue = "2023"
keychainController.containsPINCodeReturnValue = true keychainController.containsPINCodeReturnValue = true
keychainController.containsPINCodeBiometricStateReturnValue = false keychainController.containsPINCodeBiometricStateReturnValue = false
XCTAssertEqual(context.viewState.numberOfUnlockAttempts, 0, "The screen should start with zero attempts.") #expect(context.viewState.numberOfUnlockAttempts == 0, "The screen should start with zero attempts.")
XCTAssertFalse(context.viewState.isSubtitleWarning, "The subtitle should start without a warning.") #expect(!context.viewState.isSubtitleWarning, "The subtitle should start without a warning.")
XCTAssertFalse(context.viewState.isLoggingOut, "The view should not start disabled.") #expect(!context.viewState.isLoggingOut, "The view should not start disabled.")
// When entering a different PIN. // When entering a different PIN.
var deferred = deferFulfillment(context.$viewState, keyPath: \.bindings.pinCode, transitionValues: ["", "2024", ""], var deferred = deferFulfillment(context.$viewState, keyPath: \.bindings.pinCode, transitionValues: ["", "2024", ""])
message: "The PIN should be entered and then cleared by the view model.")
context.pinCode = "2024" context.pinCode = "2024"
try await deferred.fulfill() try await deferred.fulfill()
// Then the PIN should be rejected and the user notified. // Then the PIN should be rejected and the user notified.
XCTAssertEqual(context.viewState.numberOfUnlockAttempts, 1, "An invalid attempt should be counted.") #expect(context.viewState.numberOfUnlockAttempts == 1, "An invalid attempt should be counted.")
XCTAssertTrue(context.viewState.isSubtitleWarning, "The subtitle should then show a warning.") #expect(context.viewState.isSubtitleWarning, "The subtitle should then show a warning.")
XCTAssertFalse(context.viewState.isLoggingOut, "The view should still work.") #expect(!context.viewState.isLoggingOut, "The view should still work.")
// When entering the same incorrect PIN twice more // When entering the same incorrect PIN twice more
deferred = deferFulfillment(context.$viewState, keyPath: \.bindings.pinCode, transitionValues: ["", "2024", ""], deferred = deferFulfillment(context.$viewState, keyPath: \.bindings.pinCode, transitionValues: ["", "2024", ""])
message: "The PIN should be entered and then cleared by the view model.")
context.pinCode = "2024" context.pinCode = "2024"
try await deferred.fulfill() try await deferred.fulfill()
deferred = deferFulfillment(context.$viewState, keyPath: \.bindings.pinCode, transitionValues: ["", "2024", ""], deferred = deferFulfillment(context.$viewState, keyPath: \.bindings.pinCode, transitionValues: ["", "2024", ""])
message: "The PIN should be entered and then cleared by the view model.")
context.pinCode = "2024" context.pinCode = "2024"
try await deferred.fulfill() try await deferred.fulfill()
// Then the user should be alerted that they're being signed out. // Then the user should be alerted that they're being signed out.
XCTAssertEqual(context.viewState.numberOfUnlockAttempts, 3, "All invalid attempts should be counted.") #expect(context.viewState.numberOfUnlockAttempts == 3, "All invalid attempts should be counted.")
XCTAssertTrue(context.viewState.isSubtitleWarning, "The subtitle should continue showing a warning.") #expect(context.viewState.isSubtitleWarning, "The subtitle should continue showing a warning.")
XCTAssertEqual(context.alertInfo?.id, .forceLogout, "An alert should be shown about a force logout.") #expect(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.isLoggingOut, "The view should become disabled.")
}
// MARK: - Helpers
private func setup(mode: AppLockSetupPINScreenMode) {
AppSettings.resetAllSettings()
keychainController = KeychainControllerMock()
appLockService = AppLockService(keychainController: keychainController, appSettings: AppSettings())
viewModel = AppLockSetupPINScreenViewModel(initialMode: mode, isMandatory: false, appLockService: appLockService)
} }
} }

View File

@@ -7,150 +7,155 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Foundation
import Testing
class AppLockTimerTests: XCTestCase { @Suite
var timer: AppLockTimer! struct AppLockTimerTests {
private let now = Date.now
let now = Date.now private var timer: AppLockTimer!
var gracePeriod: TimeInterval { var gracePeriod: TimeInterval {
timer.gracePeriod timer.gracePeriod
} }
var halfGracePeriod: TimeInterval { var halfGracePeriod: TimeInterval {
gracePeriod / 2 timer.gracePeriod / 2
} }
var gracePeriodX2: TimeInterval { var gracePeriodX2: TimeInterval {
gracePeriod * 2 timer.gracePeriod * 2
} }
var gracePeriodX10: TimeInterval { var gracePeriodX10: TimeInterval {
gracePeriod * 10 timer.gracePeriod * 10
} }
override func tearDown() { @Test
timer = nil 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() { @Test
setupTimer(unlocked: false) mutating func timerBeforeFirstUnlock() {
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now), setupTimer(unlocked: false, backgroundedAt: now)
"The app should be locked on a fresh launch.") #expect(timer.computeLockState(didBecomeActiveAt: now),
"The app should always remain locked after backgrounding when locked.")
setupTimer(unlocked: false) setupTimer(unlocked: false, backgroundedAt: now)
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + 1), #expect(timer.computeLockState(didBecomeActiveAt: now + 1),
"The app should be locked after a fresh launch.") "The app should always remain locked after backgrounding when locked.")
setupTimer(unlocked: false) setupTimer(unlocked: false, backgroundedAt: now)
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + halfGracePeriod), #expect(timer.computeLockState(didBecomeActiveAt: now + halfGracePeriod),
"The app should be locked after a fresh launch.") "The app should always remain locked after backgrounding when locked.")
setupTimer(unlocked: false) setupTimer(unlocked: false, backgroundedAt: now)
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + gracePeriod), #expect(timer.computeLockState(didBecomeActiveAt: now + gracePeriod),
"The app should be locked after a fresh launch.") "The app should always remain locked after backgrounding when locked.")
setupTimer(unlocked: false) setupTimer(unlocked: false, backgroundedAt: now)
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + gracePeriodX10), #expect(timer.computeLockState(didBecomeActiveAt: now + gracePeriodX10),
"The app should be locked after a fresh launch.") "The app should always remain locked after backgrounding when locked.")
} }
func testTimerBeforeFirstUnlock() { @Test
setupTimer(unlocked: false, backgroundedAt: now) mutating func timerWhenUnlocked() {
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() {
setupTimer(unlocked: true, backgroundedAt: now) setupTimer(unlocked: true, backgroundedAt: now)
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: now + 1), #expect(!timer.computeLockState(didBecomeActiveAt: now + 1),
"The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.") "The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.")
setupTimer(unlocked: true, backgroundedAt: now) setupTimer(unlocked: true, backgroundedAt: now)
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: now + halfGracePeriod), #expect(!timer.computeLockState(didBecomeActiveAt: now + halfGracePeriod),
"The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.") "The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.")
setupTimer(unlocked: true, backgroundedAt: now) setupTimer(unlocked: true, backgroundedAt: now)
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + gracePeriod), #expect(timer.computeLockState(didBecomeActiveAt: now + gracePeriod),
"The app should become locked when it was unlocked and backgrounded for more than the grace period.") "The app should become locked when it was unlocked and backgrounded for more than the grace period.")
setupTimer(unlocked: true, backgroundedAt: now) setupTimer(unlocked: true, backgroundedAt: now)
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + gracePeriodX10), #expect(timer.computeLockState(didBecomeActiveAt: now + gracePeriodX10),
"The app should become locked when it was unlocked and backgrounded for more than the grace period.") "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) setupTimer(unlocked: true, backgroundedAt: now)
var nextCheck = now + halfGracePeriod var nextCheck = now + halfGracePeriod
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: nextCheck), #expect(!timer.computeLockState(didBecomeActiveAt: nextCheck),
"The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.") "The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.")
timer.applicationDidEnterBackground(date: nextCheck) timer.applicationDidEnterBackground(date: nextCheck)
nextCheck = now + gracePeriod nextCheck = now + gracePeriod
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: nextCheck), #expect(!timer.computeLockState(didBecomeActiveAt: nextCheck),
"The app should remain unlocked when repeating the backgrounded and foreground within the grace period.") "The app should remain unlocked when repeating the backgrounded and foreground within the grace period.")
timer.applicationDidEnterBackground(date: nextCheck) timer.applicationDidEnterBackground(date: nextCheck)
nextCheck = now + gracePeriod + halfGracePeriod nextCheck = now + gracePeriod + halfGracePeriod
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: nextCheck), #expect(!timer.computeLockState(didBecomeActiveAt: nextCheck),
"The app should remain unlocked when repeating the backgrounded and foreground within the grace period.") "The app should remain unlocked when repeating the backgrounded and foreground within the grace period.")
timer.applicationDidEnterBackground(date: nextCheck) timer.applicationDidEnterBackground(date: nextCheck)
nextCheck = now + gracePeriodX2 nextCheck = now + gracePeriodX2
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: nextCheck), #expect(!timer.computeLockState(didBecomeActiveAt: nextCheck),
"The app should remain unlocked when repeating the backgrounded and foreground within the grace period.") "The app should remain unlocked when repeating the backgrounded and foreground within the grace period.")
timer.applicationDidEnterBackground(date: nextCheck) timer.applicationDidEnterBackground(date: nextCheck)
nextCheck = now + gracePeriodX10 nextCheck = now + gracePeriodX10
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: nextCheck), #expect(timer.computeLockState(didBecomeActiveAt: nextCheck),
"The app should become locked however when finally staying backgrounded for longer than the grace period.") "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) setupTimer(unlocked: true)
let backgroundDate = now + gracePeriodX10 let backgroundDate = now + gracePeriodX10
timer.applicationDidEnterBackground(date: backgroundDate) timer.applicationDidEnterBackground(date: backgroundDate)
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: backgroundDate + 1), #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.") "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) setupTimer(unlocked: true, backgroundedAt: now)
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now - 1), #expect(timer.computeLockState(didBecomeActiveAt: now - 1),
"The the device's clock is changed to before the app was backgrounded, the device should remain locked.") "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. // Given a timer with no grace period that is in the background.
setupTimer(gracePeriod: 0, unlocked: true) setupTimer(gracePeriod: 0, unlocked: true)
let backgroundDate = now + 1 let backgroundDate = now + 1
timer.applicationDidEnterBackground(date: backgroundDate) timer.applicationDidEnterBackground(date: backgroundDate)
// Then the app should be locked immediately. // 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. // Given a timer with no grace period.
setupTimer(gracePeriod: 0, unlocked: true) setupTimer(gracePeriod: 0, unlocked: true)
@@ -158,36 +163,32 @@ class AppLockTimerTests: XCTestCase {
timer.applicationDidEnterBackground(date: now) timer.applicationDidEnterBackground(date: now)
// Then the app should be locked. // 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. // When the app resigns active but doesn't enter the background.
// (Nothing to do here, we just don't call applicationDidEnterBackground). // (Nothing to do here, we just don't call applicationDidEnterBackground).
// Then the app should also remain locked. // 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) // When unlocking the app and resigning active (but not entering the background)
timer.registerUnlock() timer.registerUnlock()
// (Again, nothing to do here for resigning active) // (Again, nothing to do here for resigning active)
// Then the app should not become locked. // Then the app should not become locked.
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: now + 3)) #expect(!timer.computeLockState(didBecomeActiveAt: now + 3))
} }
// MARK: - Helpers // MARK: - Helpers
/// Sets up the timer for testing. private mutating func setupTimer(gracePeriod: TimeInterval = 180, unlocked: Bool, backgroundedAt backgroundedDate: Date? = nil) {
/// - Parameters: let timer = AppLockTimer(gracePeriod: gracePeriod)
/// - 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)
if unlocked { if unlocked {
timer.registerUnlock() timer.registerUnlock()
} }
if let backgroundedDate { if let backgroundedDate {
timer.applicationDidEnterBackground(date: backgroundedDate) timer.applicationDidEnterBackground(date: backgroundedDate)
} }
self.timer = timer
} }
} }

View File

@@ -7,16 +7,18 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
class PINTextFieldTests: XCTestCase { @Suite
func testSanitize() { struct PINTextFieldTests {
@Test
func sanitize() {
let textField = PINTextField(pinCode: .constant("")) let textField = PINTextField(pinCode: .constant(""))
XCTAssertEqual(textField.sanitize("2"), "2") #expect(textField.sanitize("2") == "2")
XCTAssertEqual(textField.sanitize("2023"), "2023") #expect(textField.sanitize("2023") == "2023")
XCTAssertEqual(textField.sanitize("20233"), "2023") #expect(textField.sanitize("20233") == "2023")
XCTAssertEqual(textField.sanitize("20x"), "20") #expect(textField.sanitize("20x") == "20")
XCTAssertEqual(textField.sanitize("20!"), "20") #expect(textField.sanitize("20!") == "20")
XCTAssertEqual(textField.sanitize("boop"), "") #expect(textField.sanitize("boop") == "")
} }
} }

View File

@@ -7,117 +7,94 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Foundation
import Testing
class AppRouteURLParserTests: XCTestCase { @Suite
var appSettings: AppSettings! struct AppRouteURLParserTests {
var appRouteURLParser: AppRouteURLParser! var appSettings: AppSettings
var appRouteURLParser: AppRouteURLParser
override func setUp() { init() {
AppSettings.resetAllSettings() AppSettings.resetAllSettings()
appSettings = AppSettings() appSettings = AppSettings()
appRouteURLParser = AppRouteURLParser(appSettings: appSettings) appRouteURLParser = AppRouteURLParser(appSettings: appSettings)
} }
func testElementCallRoutes() { @Test
guard let url = URL(string: "https://call.element.io/test") else { func elementCallRoutes() throws {
XCTFail("URL invalid") let url = try #require(URL(string: "https://call.element.io/test"))
return
}
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 { let customSchemeURL = try #require(URL(string: "io.element.call:/?url=https%3A%2F%2Fcall.element.io%2Ftest"))
XCTFail("URL invalid")
return
}
XCTAssertEqual(appRouteURLParser.route(from: customSchemeURL), AppRoute.genericCallLink(url: url)) #expect(appRouteURLParser.route(from: customSchemeURL) == AppRoute.genericCallLink(url: url))
} }
func testCustomDomainUniversalLinkCallRoutes() { @Test
guard let url = URL(string: "https://somecustomdomain.element.io/test") else { func customDomainUniversalLinkCallRoutes() throws {
XCTFail("URL invalid") let url = try #require(URL(string: "https://somecustomdomain.element.io/test"))
return
}
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" let urlString = "https://somecustomdomain.element.io/test?param=123"
guard let url = URL(string: urlString) else { let url = try #require(URL(string: urlString))
XCTFail("URL invalid")
return
}
guard let encodedURLString = urlString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else { let encodedURLString = try #require(urlString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed))
XCTFail("Could not encode URL string")
return
}
guard let customSchemeURL = URL(string: "io.element.call:/?url=\(encodedURLString)") else { let customSchemeURL = try #require(URL(string: "io.element.call:/?url=\(encodedURLString)"))
XCTFail("URL invalid")
return
}
XCTAssertEqual(appRouteURLParser.route(from: customSchemeURL), AppRoute.genericCallLink(url: url)) #expect(appRouteURLParser.route(from: customSchemeURL) == AppRoute.genericCallLink(url: url))
} }
func testHttpCustomSchemeLinkCallRoutes() { @Test
guard let customSchemeURL = URL(string: "io.element.call:/?url=http%3A%2F%2Fcall.element.io%2Ftest") else { func httpCustomSchemeLinkCallRoutes() throws {
XCTFail("URL invalid") let customSchemeURL = try #require(URL(string: "io.element.call:/?url=http%3A%2F%2Fcall.element.io%2Ftest"))
return
}
XCTAssertEqual(appRouteURLParser.route(from: customSchemeURL), nil) #expect(appRouteURLParser.route(from: customSchemeURL) == nil)
} }
func testMatrixUserURL() { @Test
func matrixUserURL() throws {
let userID = "@test:matrix.org" let userID = "@test:matrix.org"
guard let url = URL(string: "https://matrix.to/#/\(userID)") else { let url = try #require(URL(string: "https://matrix.to/#/\(userID)"))
XCTFail("Invalid url")
return
}
let route = appRouteURLParser.route(from: url) 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" let id = "!abcdefghijklmnopqrstuvwxyz1234567890:matrix.org"
guard let url = URL(string: "https://matrix.to/#/\(id)") else { let url = try #require(URL(string: "https://matrix.to/#/\(id)"))
XCTFail("Invalid url")
return
}
let route = appRouteURLParser.route(from: url) 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" let id = "!abcdefghijklmnopqrstuvwxyz1234567890:matrix.org"
guard let url = URL(string: "https://app.element.io/#/room/\(id)") else { let url = try #require(URL(string: "https://app.element.io/#/room/\(id)"))
XCTFail("URL invalid")
return
}
let route = appRouteURLParser.route(from: url) 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" let id = "@alice:matrix.org"
guard let url = URL(string: "https://develop.element.io/#/user/\(id)") else { let url = try #require(URL(string: "https://develop.element.io/#/user/\(id)"))
XCTFail("URL invalid")
return
}
let route = appRouteURLParser.route(from: url) let route = appRouteURLParser.route(from: url)
XCTAssertEqual(route, .userProfile(userID: id)) #expect(route == .userProfile(userID: id))
} }
} }

View File

@@ -8,28 +8,30 @@
@testable import ElementX @testable import ElementX
import Foundation import Foundation
import XCTest import Testing
class ArrayTests: XCTestCase { @Suite
func testGrouping() { struct ArrayTests {
XCTAssertEqual([].groupBy { $0 == 0 }, []) @Test
func grouping() {
#expect([].groupBy { $0 == 0 } == [])
XCTAssertEqual([0].groupBy { $0 == 0 }, [[0]]) #expect([0].groupBy { $0 == 0 } == [[0]])
XCTAssertEqual([1].groupBy { $0 == 0 }, [[1]]) #expect([1].groupBy { $0 == 0 } == [[1]])
XCTAssertEqual([0, 0, 0].groupBy { $0 == 0 }, [[0, 0, 0]]) #expect([0, 0, 0].groupBy { $0 == 0 } == [[0, 0, 0]])
XCTAssertEqual([1, 1, 1].groupBy { $0 == 0 }, [[1], [1], [1]]) #expect([1, 1, 1].groupBy { $0 == 0 } == [[1], [1], [1]])
XCTAssertEqual([1, 0, 0, 1].groupBy { $0 == 0 }, [[1], [0, 0], [1]]) #expect([1, 0, 0, 1].groupBy { $0 == 0 } == [[1], [0, 0], [1]])
XCTAssertEqual([0, 0, 1, 0].groupBy { $0 == 0 }, [[0, 0], [1], [0]]) #expect([0, 0, 1, 0].groupBy { $0 == 0 } == [[0, 0], [1], [0]])
XCTAssertEqual([0, 0, 0, 1, 2, 3, 0].groupBy { $0 == 0 }, [[0, 0, 0], [1], [2], [3], [0]]) #expect([0, 0, 0, 1, 2, 3, 0].groupBy { $0 == 0 } == [[0, 0, 0], [1], [2], [3], [0]])
XCTAssertEqual([0, 0, 0, 1, 2, 3, 0, 0].groupBy { $0 == 0 }, [[0, 0, 0], [1], [2], [3], [0, 0]]) #expect([0, 0, 0, 1, 2, 3, 0, 0].groupBy { $0 == 0 } == [[0, 0, 0], [1], [2], [3], [0, 0]])
XCTAssertEqual([0, 0, 0, 1, 0, 2, 3, 0, 0].groupBy { $0 == 0 }, [[0, 0, 0], [1], [0], [2], [3], [0, 0]]) #expect([0, 0, 0, 1, 0, 2, 3, 0, 0].groupBy { $0 == 0 } == [[0, 0, 0], [1], [0], [2], [3], [0, 0]])
} }
} }

View File

@@ -7,31 +7,30 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
class AttributedStringTests: XCTestCase { @Suite
func testReplacingFontWithPresentationIntent() { struct AttributedStringTests {
@Test
func replacingFontWithPresentationIntent() throws {
// Given a string parsed from HTML that contains specific fixed size fonts. // Given a string parsed from HTML that contains specific fixed size fonts.
let boldString = "Bold" let boldString = "Bold"
guard let originalString = AttributedStringBuilder(mentionBuilder: MentionBuilder()) let originalString = try #require(AttributedStringBuilder(mentionBuilder: MentionBuilder())
.fromHTML("Normal <b>\(boldString)</b> Normal.") else { .fromHTML("Normal <b>\(boldString)</b> Normal."))
XCTFail("The attributed string should be built from the HTML.")
return
}
// When replacing the font with a presentation intent. // When replacing the font with a presentation intent.
let string = originalString.replacingFontWithPresentationIntent() let string = originalString.replacingFontWithPresentationIntent()
// Then the font should be removed with an inline presentation intent applied to the bold text. // Then the font should be removed with an inline presentation intent applied to the bold text.
for run in string.runs { for run in string.runs {
XCTAssertNil(run.uiKit.font, "The UIFont should have been removed.") #expect(run.uiKit.font == nil, "The UIFont should have been removed.")
XCTAssertNil(run.font, "No font should be in the run at all.") #expect(run.font == nil, "No font should be in the run at all.")
let substring = string[run.range] let substring = string[run.range]
if String(substring.characters) == boldString { 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 { } else {
XCTAssertNil(run.presentationIntent, "The rest should be plain.") #expect(run.presentationIntent == nil, "The rest should be plain.")
} }
} }
} }

View File

@@ -9,10 +9,11 @@
import Combine import Combine
@testable import ElementX @testable import ElementX
import Foundation import Foundation
import XCTest import Testing
@MainActor @MainActor
class AudioPlayerStateTests: XCTestCase { @Suite
struct AudioPlayerStateTests {
static let audioDuration = 10.0 static let audioDuration = 10.0
private var audioPlayerState: AudioPlayerState! private var audioPlayerState: AudioPlayerState!
private var audioPlayerMock: AudioPlayerMock! private var audioPlayerMock: AudioPlayerMock!
@@ -36,39 +37,42 @@ class AudioPlayerStateTests: XCTestCase {
return audioPlayerMock return audioPlayerMock
} }
override func setUp() async throws { init() async {
audioPlayerActionsSubject = .init() audioPlayerActionsSubject = .init()
audioPlayerSeekCallsSubject = .init() audioPlayerSeekCallsSubject = .init()
audioPlayerState = AudioPlayerState(id: .timelineItemIdentifier(.randomEvent), title: "", duration: Self.audioDuration) audioPlayerState = AudioPlayerState(id: .timelineItemIdentifier(.randomEvent), title: "", duration: Self.audioDuration)
audioPlayerMock = buildAudioPlayerMock() audioPlayerMock = buildAudioPlayerMock()
audioPlayerMock.seekToClosure = { [weak self] progress in audioPlayerMock.seekToClosure = { [audioPlayerMock] progress in
self?.audioPlayerMock.currentTime = Self.audioDuration * progress audioPlayerMock?.currentTime = Self.audioDuration * progress
} }
} }
func testAttach() { @Test
func attach() {
audioPlayerState.attachAudioPlayer(audioPlayerMock) audioPlayerState.attachAudioPlayer(audioPlayerMock)
XCTAssert(audioPlayerState.isAttached) #expect(audioPlayerState.isAttached)
XCTAssertEqual(audioPlayerState.playbackState, .loading) #expect(audioPlayerState.playbackState == .loading)
} }
func testDetach() { @Test
mutating func detach() {
audioPlayerState.attachAudioPlayer(audioPlayerMock) audioPlayerState.attachAudioPlayer(audioPlayerMock)
audioPlayerState.detachAudioPlayer() audioPlayerState.detachAudioPlayer()
XCTAssert(audioPlayerMock.stopCalled) #expect(audioPlayerMock.stopCalled)
XCTAssertFalse(audioPlayerState.isAttached) #expect(!audioPlayerState.isAttached)
XCTAssertEqual(audioPlayerState.playbackState, .stopped) #expect(audioPlayerState.playbackState == .stopped)
XCTAssertFalse(audioPlayerState.showProgressIndicator) #expect(!audioPlayerState.showProgressIndicator)
} }
func testDelayedState() async throws { @Test
func delayedState() async throws {
audioPlayerState.attachAudioPlayer(audioPlayerMock) audioPlayerState.attachAudioPlayer(audioPlayerMock)
XCTAssert(audioPlayerState.isAttached) #expect(audioPlayerState.isAttached)
XCTAssertEqual(audioPlayerState.playbackState, .loading) #expect(audioPlayerState.playbackState == .loading)
XCTAssertEqual(audioPlayerState.playerButtonPlaybackState, .stopped) #expect(audioPlayerState.playerButtonPlaybackState == .stopped)
let deferred = deferFulfillment(audioPlayerState.$playerButtonPlaybackState) { output in let deferred = deferFulfillment(audioPlayerState.$playerButtonPlaybackState) { output in
switch output { switch output {
@@ -80,13 +84,14 @@ class AudioPlayerStateTests: XCTestCase {
} }
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(audioPlayerState.playerButtonPlaybackState, .loading) #expect(audioPlayerState.playerButtonPlaybackState == .loading)
} }
func testOtherActionsAreNotDelayed() async throws { @Test
func otherActionsAreNotDelayed() async throws {
audioPlayerState.attachAudioPlayer(audioPlayerMock) audioPlayerState.attachAudioPlayer(audioPlayerMock)
XCTAssertEqual(audioPlayerState.playbackState, .loading) #expect(audioPlayerState.playbackState == .loading)
XCTAssertEqual(audioPlayerState.playerButtonPlaybackState, .stopped) #expect(audioPlayerState.playerButtonPlaybackState == .stopped)
let deferred = deferFulfillment(audioPlayerState.$playerButtonPlaybackState) { output in let deferred = deferFulfillment(audioPlayerState.$playerButtonPlaybackState) { output in
switch output { switch output {
@@ -99,51 +104,46 @@ class AudioPlayerStateTests: XCTestCase {
audioPlayerActionsSubject.send(.didStartPlaying) audioPlayerActionsSubject.send(.didStartPlaying)
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(audioPlayerState.playbackState, .playing) #expect(audioPlayerState.playbackState == .playing)
XCTAssertEqual(audioPlayerState.playerButtonPlaybackState, .playing) #expect(audioPlayerState.playerButtonPlaybackState == .playing)
} }
func testReportError() { @Test
XCTAssertEqual(audioPlayerState.playbackState, .stopped) mutating func reportError() {
#expect(audioPlayerState.playbackState == .stopped)
audioPlayerState.reportError() audioPlayerState.reportError()
XCTAssertEqual(audioPlayerState.playbackState, .error) #expect(audioPlayerState.playbackState == .error)
} }
func testUpdateProgress() async { @Test
mutating func updateProgress() async {
audioPlayerState.attachAudioPlayer(audioPlayerMock) audioPlayerState.attachAudioPlayer(audioPlayerMock)
// If we try to set a negative progress, the new progress must be 0.0 // If we try to set a negative progress, the new progress must be 0.0
do { await audioPlayerState.updateState(progress: -5.0)
await audioPlayerState.updateState(progress: -5.0) #expect(audioPlayerState.progress == 0.0)
XCTAssertEqual(audioPlayerState.progress, 0.0) #expect(audioPlayerMock.seekToReceivedProgress == 0.0)
XCTAssertEqual(audioPlayerMock.seekToReceivedProgress, 0.0)
}
// If we try to set a progress > 1.0, the new progress must be 1.0 // If we try to set a progress > 1.0, the new progress must be 1.0
do { await audioPlayerState.updateState(progress: 1.5)
await audioPlayerState.updateState(progress: 1.5) #expect(audioPlayerState.progress == 1.0)
XCTAssertEqual(audioPlayerState.progress, 1.0) #expect(audioPlayerMock.seekToReceivedProgress == 1.0)
XCTAssertEqual(audioPlayerMock.seekToReceivedProgress, 1.0)
}
do { audioPlayerMock.state = .stopped
audioPlayerMock.state = .stopped await audioPlayerState.updateState(progress: 0.4)
await audioPlayerState.updateState(progress: 0.4) #expect(audioPlayerState.progress == 0.4)
XCTAssertEqual(audioPlayerState.progress, 0.4) #expect(audioPlayerMock.seekToReceivedProgress == 0.4)
XCTAssertEqual(audioPlayerMock.seekToReceivedProgress, 0.4) #expect(!audioPlayerState.isPublishingProgress)
XCTAssertFalse(audioPlayerState.isPublishingProgress)
}
do { audioPlayerMock.state = .playing
audioPlayerMock.state = .playing await audioPlayerState.updateState(progress: 0.4)
await audioPlayerState.updateState(progress: 0.4) #expect(audioPlayerState.progress == 0.4)
XCTAssertEqual(audioPlayerState.progress, 0.4) #expect(audioPlayerMock.seekToReceivedProgress == 0.4)
XCTAssertEqual(audioPlayerMock.seekToReceivedProgress, 0.4) #expect(audioPlayerState.isPublishingProgress)
XCTAssert(audioPlayerState.isPublishingProgress)
}
} }
func testHandlingAudioPlayerActionDidStartLoading() async throws { @Test
func handlingAudioPlayerActionDidStartLoading() async throws {
audioPlayerState.attachAudioPlayer(audioPlayerMock) audioPlayerState.attachAudioPlayer(audioPlayerMock)
let deferred = deferFulfillment(audioPlayerState.$playbackState) { action in let deferred = deferFulfillment(audioPlayerState.$playbackState) { action in
@@ -157,10 +157,11 @@ class AudioPlayerStateTests: XCTestCase {
audioPlayerActionsSubject.send(.didStartLoading) audioPlayerActionsSubject.send(.didStartLoading)
try await deferred.fulfill() 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 audioPlayerMock.duration = 10.0
audioPlayerState = AudioPlayerState(id: .timelineItemIdentifier(.randomEvent), title: "", duration: 0) audioPlayerState = AudioPlayerState(id: .timelineItemIdentifier(.randomEvent), title: "", duration: 0)
@@ -179,12 +180,13 @@ class AudioPlayerStateTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
// The state is expected to be .readyToPlay // 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 // 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) await audioPlayerState.updateState(progress: 0.4)
audioPlayerState.attachAudioPlayer(audioPlayerMock) audioPlayerState.attachAudioPlayer(audioPlayerMock)
@@ -199,13 +201,14 @@ class AudioPlayerStateTests: XCTestCase {
audioPlayerActionsSubject.send(.didStartPlaying) audioPlayerActionsSubject.send(.didStartPlaying)
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(audioPlayerMock.seekToReceivedProgress, 0.4) #expect(audioPlayerMock.seekToReceivedProgress == 0.4)
XCTAssertEqual(audioPlayerState.playbackState, .playing) #expect(audioPlayerState.playbackState == .playing)
XCTAssert(audioPlayerState.isPublishingProgress) #expect(audioPlayerState.isPublishingProgress)
XCTAssert(audioPlayerState.showProgressIndicator) #expect(audioPlayerState.showProgressIndicator)
} }
func testHandlingAudioPlayerActionDidPausePlaying() async throws { @Test
mutating func handlingAudioPlayerActionDidPausePlaying() async throws {
await audioPlayerState.updateState(progress: 0.4) await audioPlayerState.updateState(progress: 0.4)
audioPlayerState.attachAudioPlayer(audioPlayerMock) audioPlayerState.attachAudioPlayer(audioPlayerMock)
@@ -220,13 +223,14 @@ class AudioPlayerStateTests: XCTestCase {
audioPlayerActionsSubject.send(.didPausePlaying) audioPlayerActionsSubject.send(.didPausePlaying)
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(audioPlayerState.playbackState, .stopped) #expect(audioPlayerState.playbackState == .stopped)
XCTAssertEqual(audioPlayerState.progress, 0.4) #expect(audioPlayerState.progress == 0.4)
XCTAssertFalse(audioPlayerState.isPublishingProgress) #expect(!audioPlayerState.isPublishingProgress)
XCTAssert(audioPlayerState.showProgressIndicator) #expect(audioPlayerState.showProgressIndicator)
} }
func testHandlingAudioPlayerActionsidStopPlaying() async throws { @Test
mutating func handlingAudioPlayerActionsidStopPlaying() async throws {
await audioPlayerState.updateState(progress: 0.4) await audioPlayerState.updateState(progress: 0.4)
audioPlayerState.attachAudioPlayer(audioPlayerMock) audioPlayerState.attachAudioPlayer(audioPlayerMock)
@@ -241,13 +245,14 @@ class AudioPlayerStateTests: XCTestCase {
audioPlayerActionsSubject.send(.didStopPlaying) audioPlayerActionsSubject.send(.didStopPlaying)
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(audioPlayerState.playbackState, .stopped) #expect(audioPlayerState.playbackState == .stopped)
XCTAssertEqual(audioPlayerState.progress, 0.4) #expect(audioPlayerState.progress == 0.4)
XCTAssertFalse(audioPlayerState.isPublishingProgress) #expect(!audioPlayerState.isPublishingProgress)
XCTAssert(audioPlayerState.showProgressIndicator) #expect(audioPlayerState.showProgressIndicator)
} }
func testAudioPlayerActionsDidFinishPlaying() async throws { @Test
mutating func audioPlayerActionsDidFinishPlaying() async throws {
await audioPlayerState.updateState(progress: 0.4) await audioPlayerState.updateState(progress: 0.4)
audioPlayerState.attachAudioPlayer(audioPlayerMock) audioPlayerState.attachAudioPlayer(audioPlayerMock)
@@ -262,14 +267,15 @@ class AudioPlayerStateTests: XCTestCase {
audioPlayerActionsSubject.send(.didFinishPlaying) audioPlayerActionsSubject.send(.didFinishPlaying)
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(audioPlayerState.playbackState, .stopped) #expect(audioPlayerState.playbackState == .stopped)
// Progress should be reset to 0 // Progress should be reset to 0
XCTAssertEqual(audioPlayerState.progress, 0.0) #expect(audioPlayerState.progress == 0.0)
XCTAssertFalse(audioPlayerState.isPublishingProgress) #expect(!audioPlayerState.isPublishingProgress)
XCTAssertFalse(audioPlayerState.showProgressIndicator) #expect(!audioPlayerState.showProgressIndicator)
} }
func testAudioPlayerActionsDidFailed() async throws { @Test
func audioPlayerActionsDidFailed() async throws {
audioPlayerState.attachAudioPlayer(audioPlayerMock) audioPlayerState.attachAudioPlayer(audioPlayerMock)
let deferredPlayingState = deferFulfillment(audioPlayerState.$playbackState) { action in let deferredPlayingState = deferFulfillment(audioPlayerState.$playbackState) { action in
@@ -282,7 +288,7 @@ class AudioPlayerStateTests: XCTestCase {
} }
audioPlayerActionsSubject.send(.didStartPlaying) audioPlayerActionsSubject.send(.didStartPlaying)
try await deferredPlayingState.fulfill() try await deferredPlayingState.fulfill()
XCTAssertFalse(audioPlayerState.showProgressIndicator) #expect(!audioPlayerState.showProgressIndicator)
let deferred = deferFulfillment(audioPlayerState.$playbackState) { action in let deferred = deferFulfillment(audioPlayerState.$playbackState) { action in
switch action { switch action {
@@ -295,8 +301,8 @@ class AudioPlayerStateTests: XCTestCase {
audioPlayerActionsSubject.send(.didFailWithError(error: AudioPlayerError.genericError)) audioPlayerActionsSubject.send(.didFailWithError(error: AudioPlayerError.genericError))
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(audioPlayerState.playbackState, .error) #expect(audioPlayerState.playbackState == .error)
XCTAssertFalse(audioPlayerState.isPublishingProgress) #expect(!audioPlayerState.isPublishingProgress)
XCTAssertFalse(audioPlayerState.showProgressIndicator) #expect(!audioPlayerState.showProgressIndicator)
} }
} }

View File

@@ -9,10 +9,11 @@
import Combine import Combine
@testable import ElementX @testable import ElementX
import Foundation import Foundation
import XCTest import Testing
@MainActor @MainActor
class AudioRecorderStateTests: XCTestCase { @Suite
struct AudioRecorderStateTests {
private var audioRecorderState: AudioRecorderState! private var audioRecorderState: AudioRecorderState!
private var audioRecorderMock: AudioRecorderMock! private var audioRecorderMock: AudioRecorderMock!
@@ -30,32 +31,36 @@ class AudioRecorderStateTests: XCTestCase {
return audioRecorderMock return audioRecorderMock
} }
override func setUp() async throws { init() async {
audioRecorderActionsSubject = .init() audioRecorderActionsSubject = .init()
audioRecorderState = AudioRecorderState() audioRecorderState = AudioRecorderState()
audioRecorderMock = buildAudioRecorderMock() audioRecorderMock = buildAudioRecorderMock()
} }
func testAttach() { @Test
func attach() {
audioRecorderState.attachAudioRecorder(audioRecorderMock) audioRecorderState.attachAudioRecorder(audioRecorderMock)
XCTAssertEqual(audioRecorderState.recordingState, .stopped) #expect(audioRecorderState.recordingState == .stopped)
} }
func testDetach() async { @Test
mutating func detach() async {
audioRecorderState.attachAudioRecorder(audioRecorderMock) audioRecorderState.attachAudioRecorder(audioRecorderMock)
audioRecorderMock.isRecording = true audioRecorderMock.isRecording = true
await audioRecorderState.detachAudioRecorder() await audioRecorderState.detachAudioRecorder()
XCTAssert(audioRecorderMock.stopRecordingCalled) #expect(audioRecorderMock.stopRecordingCalled)
XCTAssertEqual(audioRecorderState.recordingState, .stopped) #expect(audioRecorderState.recordingState == .stopped)
} }
func testReportError() { @Test
XCTAssertEqual(audioRecorderState.recordingState, .stopped) mutating func reportError() {
#expect(audioRecorderState.recordingState == .stopped)
audioRecorderState.reportError() audioRecorderState.reportError()
XCTAssertEqual(audioRecorderState.recordingState, .error) #expect(audioRecorderState.recordingState == .error)
} }
func testHandlingAudioRecorderActionDidStartRecording() async throws { @Test
func handlingAudioRecorderActionDidStartRecording() async throws {
audioRecorderState.attachAudioRecorder(audioRecorderMock) audioRecorderState.attachAudioRecorder(audioRecorderMock)
let deferred = deferFulfillment(audioRecorderState.$recordingState) { action in let deferred = deferFulfillment(audioRecorderState.$recordingState) { action in
@@ -69,10 +74,11 @@ class AudioRecorderStateTests: XCTestCase {
audioRecorderActionsSubject.send(.didStartRecording) audioRecorderActionsSubject.send(.didStartRecording)
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(audioRecorderState.recordingState, .recording) #expect(audioRecorderState.recordingState == .recording)
} }
func testHandlingAudioPlayerActionDidStopRecording() async throws { @Test
func handlingAudioPlayerActionDidStopRecording() async throws {
audioRecorderState.attachAudioRecorder(audioRecorderMock) audioRecorderState.attachAudioRecorder(audioRecorderMock)
let deferred = deferFulfillment(audioRecorderState.$recordingState) { action in let deferred = deferFulfillment(audioRecorderState.$recordingState) { action in
@@ -88,6 +94,6 @@ class AudioRecorderStateTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
// The state is expected to be .readyToPlay // The state is expected to be .readyToPlay
XCTAssertEqual(audioRecorderState.recordingState, .stopped) #expect(audioRecorderState.recordingState == .stopped)
} }
} }

View File

@@ -9,14 +9,15 @@
import Combine import Combine
@testable import ElementX @testable import ElementX
import Foundation import Foundation
import XCTest import Testing
@MainActor @MainActor
class AudioRecorderTests: XCTestCase { @Suite
struct AudioRecorderTests {
private var audioRecorder: AudioRecorder! private var audioRecorder: AudioRecorder!
private var audioSessionMock: AudioSessionMock! private var audioSessionMock: AudioSessionMock!
override func setUp() async throws { init() async {
audioSessionMock = AudioSessionMock() audioSessionMock = AudioSessionMock()
audioSessionMock.requestRecordPermissionClosure = { completion in audioSessionMock.requestRecordPermissionClosure = { completion in
completion(true) completion(true)
@@ -24,11 +25,8 @@ class AudioRecorderTests: XCTestCase {
audioRecorder = AudioRecorder(audioSession: audioSessionMock) audioRecorder = AudioRecorder(audioSession: audioSessionMock)
} }
override func tearDown() async throws { @Test
await audioRecorder?.cancelRecording() mutating func recordWithoutPermission() async throws {
}
func testRecordWithoutPermission() async throws {
audioSessionMock.requestRecordPermissionClosure = { completion in audioSessionMock.requestRecordPermissionClosure = { completion in
completion(false) completion(false)
} }
@@ -44,6 +42,6 @@ class AudioRecorderTests: XCTestCase {
let url = URL.temporaryDirectory.appendingPathComponent("test-voice-message").appendingPathExtension("m4a") let url = URL.temporaryDirectory.appendingPathComponent("test-voice-message").appendingPathExtension("m4a")
await audioRecorder.record(audioFileURL: url) await audioRecorder.record(audioFileURL: url)
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertFalse(audioRecorder.isRecording) #expect(!audioRecorder.isRecording)
} }
} }

View File

@@ -7,86 +7,93 @@
// //
@testable import ElementX @testable import ElementX
import Foundation
import MatrixRustSDKMocks import MatrixRustSDKMocks
import XCTest import Testing
class AuthenticationServiceTests: XCTestCase { @Suite
@MainActor
struct AuthenticationServiceTests {
var client: ClientSDKMock! var client: ClientSDKMock!
var userSessionStore: UserSessionStoreMock! var userSessionStore: UserSessionStoreMock!
var encryptionKeyProvider: MockEncryptionKeyProvider! var encryptionKeyProvider: MockEncryptionKeyProvider!
var service: AuthenticationService! var service: AuthenticationService!
func testPasswordLogin() async { @Test
setupMocks(serverAddress: "example.com") mutating func passwordLogin() async {
setup(serverAddress: "example.com")
switch await service.configure(for: "example.com", flow: .login) { switch await service.configure(for: "example.com", flow: .login) {
case .success: case .success:
break break
case .failure(let error): case .failure(let error):
XCTFail("Unexpected failure: \(error)") Issue.record("Unexpected failure: \(error)")
} }
XCTAssertEqual(service.flow, .login) #expect(service.flow == .login)
XCTAssertEqual(service.homeserver.value, .mockBasicServer) #expect(service.homeserver.value == .mockBasicServer)
switch await service.login(username: "alice", password: "12345678", initialDeviceName: nil, deviceID: nil) { switch await service.login(username: "alice", password: "12345678", initialDeviceName: nil, deviceID: nil) {
case .success: case .success:
XCTAssertEqual(client.loginUsernamePasswordInitialDeviceNameDeviceIdCallsCount, 1) #expect(client.loginUsernamePasswordInitialDeviceNameDeviceIdCallsCount == 1)
XCTAssertEqual(userSessionStore.userSessionForSessionDirectoriesPassphraseCallsCount, 1) #expect(userSessionStore.userSessionForSessionDirectoriesPassphraseCallsCount == 1)
XCTAssertEqual(userSessionStore.userSessionForSessionDirectoriesPassphraseReceivedArguments?.passphrase, #expect(userSessionStore.userSessionForSessionDirectoriesPassphraseReceivedArguments?.passphrase ==
encryptionKeyProvider.generateKey().base64EncodedString()) encryptionKeyProvider.generateKey().base64EncodedString())
case .failure(let error): case .failure(let error):
XCTFail("Unexpected failure: \(error)") Issue.record("Unexpected failure: \(error)")
} }
} }
func testConfigureLoginWithOIDC() async { @Test
setupMocks() mutating func configureLoginWithOIDC() async {
setup()
switch await service.configure(for: "matrix.org", flow: .login) { switch await service.configure(for: "matrix.org", flow: .login) {
case .success: case .success:
break break
case .failure(let error): case .failure(let error):
XCTFail("Unexpected failure: \(error)") Issue.record("Unexpected failure: \(error)")
} }
XCTAssertEqual(service.flow, .login) #expect(service.flow == .login)
XCTAssertEqual(service.homeserver.value, .mockMatrixDotOrg) #expect(service.homeserver.value == .mockMatrixDotOrg)
} }
func testConfigureRegisterWithOIDC() async { @Test
setupMocks() mutating func configureRegisterWithOIDC() async {
setup()
switch await service.configure(for: "matrix.org", flow: .register) { switch await service.configure(for: "matrix.org", flow: .register) {
case .success: case .success:
break break
case .failure(let error): case .failure(let error):
XCTFail("Unexpected failure: \(error)") Issue.record("Unexpected failure: \(error)")
} }
XCTAssertEqual(service.flow, .register) #expect(service.flow == .register)
XCTAssertEqual(service.homeserver.value, .mockMatrixDotOrg) #expect(service.homeserver.value == .mockMatrixDotOrg)
} }
func testConfigureRegisterNoSupport() async { @Test
@MainActor
mutating func configureRegisterNoSupport() async {
let homeserverAddress = "example.com" let homeserverAddress = "example.com"
setupMocks(serverAddress: homeserverAddress) setup(serverAddress: homeserverAddress)
switch await service.configure(for: homeserverAddress, flow: .register) { switch await service.configure(for: homeserverAddress, flow: .register) {
case .success: case .success:
XCTFail("Configuration should have failed") Issue.record("Configuration should have failed")
case .failure(let error): case .failure(let error):
XCTAssertEqual(error, .registrationNotSupported) #expect(error == .registrationNotSupported)
} }
XCTAssertEqual(service.flow, .login) #expect(service.flow == .login)
XCTAssertEqual(service.homeserver.value, .init(address: "matrix.org", loginMode: .unknown)) #expect(service.homeserver.value == .init(address: "matrix.org", loginMode: .unknown))
} }
// MARK: - Helpers // MARK: - Helpers
private func setupMocks(serverAddress: String = "matrix.org") { private mutating func setup(serverAddress: String = "matrix.org") {
let configuration: AuthenticationClientFactoryMock.Configuration = .init() let configuration: AuthenticationClientFactoryMock.Configuration = .init()
let clientFactory = AuthenticationClientFactoryMock(configuration: configuration) let clientFactory = AuthenticationClientFactoryMock(configuration: configuration)

View File

@@ -8,10 +8,12 @@
@testable import ElementX @testable import ElementX
import MatrixRustSDKMocks import MatrixRustSDKMocks
import XCTest import Testing
import UIKit
@MainActor @MainActor
class AuthenticationStartScreenViewModelTests: XCTestCase { @Suite
final class AuthenticationStartScreenViewModelTests {
var clientFactory: AuthenticationClientFactoryMock! var clientFactory: AuthenticationClientFactoryMock!
var client: ClientSDKMock! var client: ClientSDKMock!
var appSettings: AppSettings! var appSettings: AppSettings!
@@ -22,22 +24,23 @@ class AuthenticationStartScreenViewModelTests: XCTestCase {
viewModel.context viewModel.context
} }
override func setUp() { init() {
AppSettings.resetAllSettings() AppSettings.resetAllSettings()
appSettings = AppSettings() appSettings = AppSettings()
// These app settings are kept local to the tests on purpose as if they are registered in the // 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. // ServiceLocator, the providers override that we apply will break other tests in the suite.
} }
override func tearDown() { deinit {
AppSettings.resetAllSettings() AppSettings.resetAllSettings()
} }
func testInitialState() async throws { @Test
func initialState() async throws {
// Given a view model that has no provisioning parameters. // Given a view model that has no provisioning parameters.
setupViewModel() setupViewModel()
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .unknown) #expect(authenticationService.homeserver.value.loginMode == .unknown)
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 0) #expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
// When tapping any of the buttons on the screen // When tapping any of the buttons on the screen
let actions: [(AuthenticationStartScreenViewAction, AuthenticationStartScreenViewModelAction)] = [ let actions: [(AuthenticationStartScreenViewAction, AuthenticationStartScreenViewModelAction)] = [
@@ -53,17 +56,18 @@ class AuthenticationStartScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
// Then the authentication service should not be used yet. // Then the authentication service should not be used yet.
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0) #expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 0) #expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .unknown) #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. // Given a view model that has been provisioned with a server that supports OIDC.
setupViewModel(provisioningParameters: .init(accountProvider: "company.com", loginHint: "user@company.com")) setupViewModel(provisioningParameters: .init(accountProvider: "company.com", loginHint: "user@company.com"))
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .unknown) #expect(authenticationService.homeserver.value.loginMode == .unknown)
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 0) #expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
// When tapping the login button the authentication service should be used and the screen // 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. // should request to continue the flow without any server selection needed.
@@ -71,18 +75,19 @@ class AuthenticationStartScreenViewModelTests: XCTestCase {
context.send(viewAction: .login) context.send(viewAction: .login)
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) #expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 1) #expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 1)
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.prompt, .consent) #expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.prompt == .consent)
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.loginHint, "user@company.com") #expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.loginHint == "user@company.com")
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .oidc(supportsCreatePrompt: false)) #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. // 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) setupViewModel(provisioningParameters: .init(accountProvider: "company.com", loginHint: "user@company.com"), supportsOIDC: false)
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .unknown) #expect(authenticationService.homeserver.value.loginMode == .unknown)
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 0) #expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
// When tapping the login button the authentication service should be used and the screen // 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. // should request to continue the flow without any server selection needed.
@@ -91,16 +96,17 @@ class AuthenticationStartScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
// Then a call to configure service should be made. // Then a call to configure service should be made.
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) #expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .password) #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. // Given a view model that for an app that only allows the use of a single provider that supports OIDC.
setAllowedAccountProviders(["company.com"]) setAllowedAccountProviders(["company.com"])
setupViewModel() setupViewModel()
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .unknown) #expect(authenticationService.homeserver.value.loginMode == .unknown)
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 0) #expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
// When tapping the login button the authentication service should be used and the screen // 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. // should request to continue the flow without any server selection needed.
@@ -108,19 +114,20 @@ class AuthenticationStartScreenViewModelTests: XCTestCase {
context.send(viewAction: .login) context.send(viewAction: .login)
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) #expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 1) #expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 1)
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.prompt, .consent) #expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.prompt == .consent)
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.loginHint, nil) #expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.loginHint == nil)
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .oidc(supportsCreatePrompt: false)) #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. // 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"]) setAllowedAccountProviders(["company.com"])
setupViewModel(supportsOIDC: false) setupViewModel(supportsOIDC: false)
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .unknown) #expect(authenticationService.homeserver.value.loginMode == .unknown)
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 0) #expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
// When tapping the login button the authentication service should be used and the screen // 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. // should request to continue the flow without any server selection needed.
@@ -129,8 +136,8 @@ class AuthenticationStartScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
// Then a call to configure service should be made. // Then a call to configure service should be made.
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) #expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .password) #expect(authenticationService.homeserver.value.loginMode == .password)
} }
// MARK: - Helpers // MARK: - Helpers

View File

@@ -8,25 +8,29 @@
import Combine import Combine
@testable import ElementX @testable import ElementX
import XCTest import Foundation
import Testing
@MainActor @MainActor
class BlockedUsersScreenViewModelTests: XCTestCase { @Suite
func testInitialState() async throws { struct BlockedUsersScreenViewModelTests {
@Test
func initialState() async throws {
let clientProxy = ClientProxyMock(.init(userID: RoomMemberProxyMock.mockMe.userID)) let clientProxy = ClientProxyMock(.init(userID: RoomMemberProxyMock.mockMe.userID))
let viewModel = BlockedUsersScreenViewModel(hideProfiles: true, let viewModel = BlockedUsersScreenViewModel(hideProfiles: true,
userSession: UserSessionMock(.init(clientProxy: clientProxy)), userSession: UserSessionMock(.init(clientProxy: clientProxy)),
userIndicatorController: ServiceLocator.shared.userIndicatorController) 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() try await deferred.fulfill()
XCTAssertFalse(viewModel.context.viewState.blockedUsers.isEmpty) #expect(!viewModel.context.viewState.blockedUsers.isEmpty)
XCTAssertFalse(clientProxy.profileForCalled) #expect(!clientProxy.profileForCalled)
} }
func testProfiles() async throws { @Test
func profiles() async throws {
let clientProxy = ClientProxyMock(.init(userID: RoomMemberProxyMock.mockMe.userID)) let clientProxy = ClientProxyMock(.init(userID: RoomMemberProxyMock.mockMe.userID))
let viewModel = BlockedUsersScreenViewModel(hideProfiles: false, 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 } } let deferred = deferFulfillment(viewModel.context.observe(\.viewState.blockedUsers)) { $0.contains { $0.displayName != nil } }
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertFalse(viewModel.context.viewState.blockedUsers.isEmpty) #expect(!viewModel.context.viewState.blockedUsers.isEmpty)
XCTAssertTrue(clientProxy.profileForCalled) #expect(clientProxy.profileForCalled)
} }
} }

View File

@@ -7,17 +7,20 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
import UIKit
@MainActor @MainActor
class BugReportScreenViewModelTests: XCTestCase { @Suite
struct BugReportScreenViewModelTests {
let logFiles: [URL] = [URL(filePath: "/path/to/file1.log"), URL(filePath: "/path/to/file2.log")] let logFiles: [URL] = [URL(filePath: "/path/to/file1.log"), URL(filePath: "/path/to/file2.log")]
enum TestError: Error { enum TestError: Error {
case testError case testError
} }
func testInitialState() { @Test
func initialState() {
let clientProxy = ClientProxyMock(.init(userID: "@mock.client.com")) let clientProxy = ClientProxyMock(.init(userID: "@mock.client.com"))
let viewModel = BugReportScreenViewModel(bugReportService: BugReportServiceMock(), let viewModel = BugReportScreenViewModel(bugReportService: BugReportServiceMock(),
clientProxy: clientProxy, clientProxy: clientProxy,
@@ -26,12 +29,13 @@ class BugReportScreenViewModelTests: XCTestCase {
isModallyPresented: false) isModallyPresented: false)
let context = viewModel.context let context = viewModel.context
XCTAssertEqual(context.reportText, "") #expect(context.reportText == "")
XCTAssertNil(context.viewState.screenshot) #expect(context.viewState.screenshot == nil)
XCTAssertTrue(context.sendingLogsEnabled) #expect(context.sendingLogsEnabled)
} }
func testClearScreenshot() { @Test
func clearScreenshot() {
let clientProxy = ClientProxyMock(.init(userID: "@mock.client.com")) let clientProxy = ClientProxyMock(.init(userID: "@mock.client.com"))
let viewModel = BugReportScreenViewModel(bugReportService: BugReportServiceMock(), let viewModel = BugReportScreenViewModel(bugReportService: BugReportServiceMock(),
clientProxy: clientProxy, clientProxy: clientProxy,
@@ -41,10 +45,11 @@ class BugReportScreenViewModelTests: XCTestCase {
let context = viewModel.context let context = viewModel.context
context.send(viewAction: .removeScreenshot) 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 clientProxy = ClientProxyMock(.init(userID: "@mock.client.com"))
let viewModel = BugReportScreenViewModel(bugReportService: BugReportServiceMock(), let viewModel = BugReportScreenViewModel(bugReportService: BugReportServiceMock(),
clientProxy: clientProxy, clientProxy: clientProxy,
@@ -52,12 +57,13 @@ class BugReportScreenViewModelTests: XCTestCase {
screenshot: nil, screenshot: nil,
isModallyPresented: false) isModallyPresented: false)
let context = viewModel.context let context = viewModel.context
XCTAssertNil(context.viewState.screenshot) #expect(context.viewState.screenshot == nil)
context.send(viewAction: .attachScreenshot(UIImage.actions)) 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() let mockService = BugReportServiceMock()
mockService.submitBugReportProgressListenerClosure = { _, _ in mockService.submitBugReportProgressListenerClosure = { _, _ in
await Task.yield() await Task.yield()
@@ -88,19 +94,20 @@ class BugReportScreenViewModelTests: XCTestCase {
context.send(viewAction: .submit) context.send(viewAction: .submit)
try await deferred.fulfill() try await deferred.fulfill()
XCTAssert(mockService.submitBugReportProgressListenerCallsCount == 1) #expect(mockService.submitBugReportProgressListenerCallsCount == 1)
XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.userID, "@mock.client.com") #expect(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.userID == "@mock.client.com")
XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.deviceID, "ABCDEFGH") #expect(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.deviceID == "ABCDEFGH")
XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.curve25519, "THECURVEKEYKEY") #expect(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.curve25519 == "THECURVEKEYKEY")
XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.ed25519, "THEEDKEYKEY") #expect(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.ed25519 == "THEEDKEYKEY")
XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.text, "This will succeed") #expect(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.text == "This will succeed")
XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.logFiles, logFiles) #expect(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.logFiles == logFiles)
XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.canContact, false) #expect(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.canContact == false)
XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.githubLabels, []) #expect(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.githubLabels == [])
XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.files, []) #expect(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.files == [])
} }
func testSendReportWithError() async throws { @Test
func sendReportWithError() async throws {
let mockService = BugReportServiceMock() let mockService = BugReportServiceMock()
mockService.submitBugReportProgressListenerClosure = { _, _ in mockService.submitBugReportProgressListenerClosure = { _, _ in
.failure(.uploadFailure(TestError.testError)) .failure(.uploadFailure(TestError.testError))
@@ -125,8 +132,8 @@ class BugReportScreenViewModelTests: XCTestCase {
context.send(viewAction: .submit) context.send(viewAction: .submit)
try await deferred.fulfill() try await deferred.fulfill()
XCTAssert(mockService.submitBugReportProgressListenerCallsCount == 1) #expect(mockService.submitBugReportProgressListenerCallsCount == 1)
XCTAssertEqual(context.reportText, "This will fail", "The bug report should remain in place so the user can retry.") #expect(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(!context.viewState.shouldDisableInteraction, "The user should be able to retry.")
} }
} }

View File

@@ -9,13 +9,14 @@
import Combine import Combine
@testable import ElementX @testable import ElementX
import Foundation import Foundation
import XCTest import Testing
class BugReportServiceTests: XCTestCase { @Suite
final class BugReportServiceTests {
var appSettings: AppSettings! var appSettings: AppSettings!
var bugReportService: BugReportServiceProtocol! var bugReportService: BugReportServiceProtocol!
override func setUpWithError() throws { init() throws {
AppSettings.resetAllSettings() AppSettings.resetAllSettings()
appSettings = AppSettings() appSettings = AppSettings()
appSettings.bugReportRageshakeURL.reset() appSettings.bugReportRageshakeURL.reset()
@@ -26,15 +27,17 @@ class BugReportServiceTests: XCTestCase {
bugReportService = bugReportServiceMock bugReportService = bugReportServiceMock
} }
override func tearDown() { deinit {
appSettings.bugReportRageshakeURL.reset() appSettings.bugReportRageshakeURL.reset()
} }
func testInitialStateWithMockService() { @Test
XCTAssertFalse(bugReportService.crashedLastRun) func initialStateWithMockService() {
#expect(!bugReportService.crashedLastRun)
} }
func testSubmitBugReportWithMockService() async throws { @Test
func submitBugReportWithMockService() async throws {
let bugReport = BugReport(userID: "@mock:client.com", let bugReport = BugReport(userID: "@mock:client.com",
deviceID: nil, deviceID: nil,
ed25519: nil, ed25519: nil,
@@ -46,33 +49,36 @@ class BugReportServiceTests: XCTestCase {
files: []) files: [])
let progressSubject = CurrentValueSubject<Double, Never>(0.0) let progressSubject = CurrentValueSubject<Double, Never>(0.0)
let response = try await bugReportService.submitBugReport(bugReport, progressListener: progressSubject).get() let response = try await bugReportService.submitBugReport(bugReport, progressListener: progressSubject).get()
let reportURL = try XCTUnwrap(response.reportURL) let reportURL = try #require(response.reportURL)
XCTAssertFalse(reportURL.isEmpty) #expect(!reportURL.isEmpty)
} }
func testInitialStateWithRealService() { @Test
func initialStateWithRealService() {
let urlPublisher: CurrentValueSubject<RageshakeConfiguration, Never> = .init(.url("https://example.com/submit")) let urlPublisher: CurrentValueSubject<RageshakeConfiguration, Never> = .init(.url("https://example.com/submit"))
let service = BugReportService(rageshakeURLPublisher: urlPublisher.asCurrentValuePublisher(), let service = BugReportService(rageshakeURLPublisher: urlPublisher.asCurrentValuePublisher(),
applicationID: "mock_app_id", applicationID: "mock_app_id",
sdkGitSHA: "1234", sdkGitSHA: "1234",
session: .mock, session: .mock,
appHooks: AppHooks()) appHooks: AppHooks())
XCTAssertTrue(service.isEnabled) #expect(service.isEnabled)
XCTAssertFalse(service.crashedLastRun) #expect(!service.crashedLastRun)
} }
func testInitialStateWithRealServiceAndDisabled() { @Test
func initialStateWithRealServiceAndDisabled() {
let urlPublisher: CurrentValueSubject<RageshakeConfiguration, Never> = .init(.disabled) let urlPublisher: CurrentValueSubject<RageshakeConfiguration, Never> = .init(.disabled)
let service = BugReportService(rageshakeURLPublisher: urlPublisher.asCurrentValuePublisher(), let service = BugReportService(rageshakeURLPublisher: urlPublisher.asCurrentValuePublisher(),
applicationID: "mock_app_id", applicationID: "mock_app_id",
sdkGitSHA: "1234", sdkGitSHA: "1234",
session: .mock, session: .mock,
appHooks: AppHooks()) appHooks: AppHooks())
XCTAssertFalse(service.isEnabled) #expect(!service.isEnabled)
XCTAssertFalse(service.crashedLastRun) #expect(!service.crashedLastRun)
} }
@MainActor func testSubmitBugReportWithRealService() async throws { @Test @MainActor
func submitBugReportWithRealService() async throws {
let urlPublisher: CurrentValueSubject<RageshakeConfiguration, Never> = .init(.url("https://example.com/submit")) let urlPublisher: CurrentValueSubject<RageshakeConfiguration, Never> = .init(.url("https://example.com/submit"))
let service = BugReportService(rageshakeURLPublisher: urlPublisher.asCurrentValuePublisher(), let service = BugReportService(rageshakeURLPublisher: urlPublisher.asCurrentValuePublisher(),
applicationID: "mock_app_id", applicationID: "mock_app_id",
@@ -92,12 +98,14 @@ class BugReportServiceTests: XCTestCase {
let progressSubject = CurrentValueSubject<Double, Never>(0.0) let progressSubject = CurrentValueSubject<Double, Never>(0.0)
let response = try await service.submitBugReport(bugReport, progressListener: progressSubject).get() 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 { guard case let .url(initialURL) = appSettings.bugReportRageshakeURL.publisher.value else {
XCTFail("Unexpected initial configuration.") Issue.record("Unexpected initial configuration.")
return return
} }
@@ -106,13 +114,13 @@ class BugReportServiceTests: XCTestCase {
sdkGitSHA: "1234", sdkGitSHA: "1234",
session: .mock, session: .mock,
appHooks: AppHooks()) appHooks: AppHooks())
XCTAssertTrue(service.isEnabled) #expect(service.isEnabled)
appSettings.bugReportRageshakeURL.applyRemoteValue(.disabled) appSettings.bugReportRageshakeURL.applyRemoteValue(.disabled)
XCTAssertFalse(service.isEnabled) #expect(!service.isEnabled)
appSettings.bugReportRageshakeURL.applyRemoteValue(.url("https://bugs.server.net/submit")) appSettings.bugReportRageshakeURL.applyRemoteValue(.url("https://bugs.server.net/submit"))
XCTAssertTrue(service.isEnabled) #expect(service.isEnabled)
let bugReport = BugReport(userID: "@mock:client.com", let bugReport = BugReport(userID: "@mock:client.com",
deviceID: nil, deviceID: nil,
@@ -126,14 +134,14 @@ class BugReportServiceTests: XCTestCase {
let progressSubject = CurrentValueSubject<Double, Never>(0.0) let progressSubject = CurrentValueSubject<Double, Never>(0.0)
let customConfigurationResponse = try await service.submitBugReport(bugReport, progressListener: progressSubject).get() 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() appSettings.bugReportRageshakeURL.reset()
XCTAssertTrue(service.isEnabled) #expect(service.isEnabled)
let defaultConfigurationResponse = try await service.submitBugReport(bugReport, progressListener: progressSubject).get() 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"))
} }
} }

View File

@@ -1,13 +0,0 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
@testable import ElementX
import XCTest
@MainActor
class CallScreenViewModelTests: XCTestCase { }

View File

@@ -7,55 +7,61 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Foundation
import Testing
// swiftlint:disable force_unwrapping @Suite
struct DateTests {
class DateTests: XCTestCase {
let calendar = Calendar.current let calendar = Calendar.current
let startOfToday = Calendar.current.startOfDay(for: .now) var startOfToday: Date {
let startOfYesterday = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: -1, to: .now)!) Calendar.current.startOfDay(for: .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()))
} }
func testDateSeparatorFormatting() throws { var startOfYesterday: Date {
let today = try XCTUnwrap(calendar.date(byAdding: DateComponents(hour: 9, minute: 30), to: startOfToday)) // swiftlint: disable:next force_unwrapping
XCTAssertEqual(today.formattedDateSeparator(), "Today") Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: -1, to: .now)!)
}
let yesterday = try XCTUnwrap(calendar.date(byAdding: .hour, value: 1, to: startOfYesterday)) @Test
XCTAssertEqual(yesterday.formattedDateSeparator(), "Yesterday") 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 nearYesterday = try XCTUnwrap(calendar.date(byAdding: DateComponents(hour: -10), to: today)) let yesterday = try #require(calendar.date(byAdding: .hour, value: 1, to: startOfYesterday))
XCTAssertEqual(nearYesterday.formattedDateSeparator(), yesterday.formatted(Date.RelativeFormatStyle(presentation: .named, capitalizationContext: .beginningOfSentence))) #expect(yesterday.formattedMinimal() == yesterday.formatted(Date.RelativeFormatStyle(presentation: .named, capitalizationContext: .beginningOfSentence)))
let threeDaysAgo = try XCTUnwrap(calendar.date(byAdding: .day, value: -3, to: startOfToday)) let nearYesterday = try #require(calendar.date(byAdding: DateComponents(hour: -10), to: today))
XCTAssertEqual(threeDaysAgo.formattedDateSeparator(), threeDaysAgo.formatted(.dateTime.weekday(.wide))) #expect(nearYesterday.formattedMinimal() == yesterday.formatted(Date.RelativeFormatStyle(presentation: .named, capitalizationContext: .beginningOfSentence)))
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. // This test will fail during the first 6 days of the year.
let startOfTheYear = try XCTUnwrap(calendar.dateInterval(of: .year, for: startOfToday)?.start) let startOfTheYear = try #require(calendar.dateInterval(of: .year, for: startOfToday)?.start)
XCTAssertEqual(startOfTheYear.formattedDateSeparator(), startOfTheYear.formatted(.dateTime.weekday(.wide).day().month(.wide))) #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))) let theMillennium = try #require(calendar.date(from: DateComponents(year: 2000, month: 1, day: 1)))
XCTAssertEqual(theMillennium.formattedDateSeparator(), theMillennium.formatted(.dateTime.weekday(.wide).day().month(.wide).year())) #expect(theMillennium.formattedDateSeparator() == theMillennium.formatted(.dateTime.weekday(.wide).day().month(.wide).year()))
} }
} }
// swiftlint:enable force_unwrapping

View File

@@ -7,10 +7,12 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Foundation
import Testing
@MainActor @MainActor
class DeactivateAccountScreenViewModelTests: XCTestCase { @Suite
struct DeactivateAccountScreenViewModelTests {
var clientProxy: ClientProxyMock! var clientProxy: ClientProxyMock!
var viewModel: DeactivateAccountScreenViewModelProtocol! var viewModel: DeactivateAccountScreenViewModelProtocol!
@@ -18,40 +20,34 @@ class DeactivateAccountScreenViewModelTests: XCTestCase {
viewModel.context viewModel.context
} }
override func setUpWithError() throws { init() {
clientProxy = ClientProxyMock(.init()) clientProxy = ClientProxyMock(.init())
viewModel = DeactivateAccountScreenViewModel(clientProxy: clientProxy, userIndicatorController: UserIndicatorControllerMock()) viewModel = DeactivateAccountScreenViewModel(clientProxy: clientProxy, userIndicatorController: UserIndicatorControllerMock())
} }
func testDeactivate() async throws { @Test
mutating func deactivate() async throws {
try await validateDeactivate(erasingData: false) try await validateDeactivate(erasingData: false)
} }
func testDeactivateAndErase() async throws { @Test
mutating func deactivateAndErase() async throws {
try await validateDeactivate(erasingData: true) try await validateDeactivate(erasingData: true)
} }
func validateDeactivate(erasingData shouldErase: Bool) async throws { mutating func validateDeactivate(erasingData shouldErase: Bool) async throws {
let enteredPassword = UUID().uuidString let enteredPassword = UUID().uuidString
clientProxy.deactivateAccountPasswordEraseDataClosure = { [weak self] password, eraseData in clientProxy.deactivateAccountPasswordEraseDataClosure = { [weak clientProxy] password, eraseData in
guard let self else { return .failure(.sdkError(ClientProxyMockError.generic)) } guard let clientProxy else { return .failure(.sdkError(ClientProxyMockError.generic)) }
if clientProxy.deactivateAccountPasswordEraseDataCallsCount == 1 { if clientProxy.deactivateAccountPasswordEraseDataCallsCount == 1 {
if password != nil { #expect(password == nil, "The password shouldn't be sent first time round.")
XCTFail("The password shouldn't be sent first time round.") #expect(eraseData == shouldErase, "The erase parameter is unexpected.")
}
if eraseData != shouldErase {
XCTFail("The erase parameter is unexpected.")
}
return .failure(.sdkError(ClientProxyMockError.generic)) return .failure(.sdkError(ClientProxyMockError.generic))
} else { } else {
if password != enteredPassword { #expect(password == enteredPassword, "The password should match the user's input on the second call.")
XCTFail("The password should match the user's input on the second call.") #expect(eraseData == shouldErase, "The erase parameter is unexpected.")
}
if eraseData != shouldErase {
XCTFail("The erase parameter is unexpected.")
}
return .success(()) return .success(())
} }
} }
@@ -59,23 +55,21 @@ class DeactivateAccountScreenViewModelTests: XCTestCase {
context.eraseData = shouldErase context.eraseData = shouldErase
context.password = enteredPassword context.password = enteredPassword
XCTAssertNil(context.alertInfo) #expect(context.alertInfo == nil)
let deferredState = deferFulfillment(context.observe(\.viewState.bindings.alertInfo)) { $0 != nil } let deferredState = deferFulfillment(context.observe(\.viewState.bindings.alertInfo)) { $0 != nil }
context.send(viewAction: .deactivate) context.send(viewAction: .deactivate)
try await deferredState.fulfill() try await deferredState.fulfill()
guard let confirmationAction = context.alertInfo?.primaryButton.action else { let confirmationAction = try #require(context.alertInfo?.primaryButton.action,
XCTFail("Couldn't find the confirmation action.") "Couldn't find the confirmation action.")
return
}
let deferredAction = deferFulfillment(viewModel.actionsPublisher) { $0 == .accountDeactivated } let deferredAction = deferFulfillment(viewModel.actionsPublisher) { $0 == .accountDeactivated }
confirmationAction() confirmationAction()
try await deferredAction.fulfill() try await deferredAction.fulfill()
XCTAssertEqual(clientProxy.deactivateAccountPasswordEraseDataCallsCount, 2) #expect(clientProxy.deactivateAccountPasswordEraseDataCallsCount == 2)
XCTAssertEqual(clientProxy.deactivateAccountPasswordEraseDataReceivedArguments?.password, enteredPassword) #expect(clientProxy.deactivateAccountPasswordEraseDataReceivedArguments?.password == enteredPassword)
XCTAssertEqual(clientProxy.deactivateAccountPasswordEraseDataReceivedArguments?.eraseData, shouldErase) #expect(clientProxy.deactivateAccountPasswordEraseDataReceivedArguments?.eraseData == shouldErase)
} }
} }

View File

@@ -7,18 +7,19 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class DeclineAndBlockScreenViewModelTests: XCTestCase { @Suite
var viewModel: DeclineAndBlockScreenViewModelProtocol! struct DeclineAndBlockScreenViewModelTests {
var clientProxy: ClientProxyMock! var viewModel: DeclineAndBlockScreenViewModelProtocol
var clientProxy: ClientProxyMock
var context: DeclineAndBlockScreenViewModelType.Context { var context: DeclineAndBlockScreenViewModelType.Context {
viewModel.context viewModel.context
} }
override func setUp() { init() {
clientProxy = ClientProxyMock(.init()) clientProxy = ClientProxyMock(.init())
viewModel = DeclineAndBlockScreenViewModel(userID: "@alice:matrix.org", viewModel = DeclineAndBlockScreenViewModel(userID: "@alice:matrix.org",
roomID: "!room:matrix.org", roomID: "!room:matrix.org",
@@ -26,39 +27,42 @@ class DeclineAndBlockScreenViewModelTests: XCTestCase {
userIndicatorController: UserIndicatorControllerMock()) userIndicatorController: UserIndicatorControllerMock())
} }
func testInitialState() { @Test
XCTAssertFalse(context.viewState.isDeclineDisabled) func initialState() {
XCTAssertFalse(context.shouldReport) #expect(!context.viewState.isDeclineDisabled)
XCTAssertTrue(context.shouldBlockUser) #expect(!context.shouldReport)
#expect(context.shouldBlockUser)
} }
func testDeclineDisabled() { @Test
mutating func declineDisabled() {
context.shouldBlockUser = false context.shouldBlockUser = false
XCTAssertTrue(context.viewState.isDeclineDisabled) #expect(context.viewState.isDeclineDisabled)
XCTAssertFalse(context.shouldReport) #expect(!context.shouldReport)
XCTAssertFalse(context.shouldBlockUser) #expect(!context.shouldBlockUser)
context.shouldReport = true context.shouldReport = true
// Should report set to `true` always requires a non empty reason // Should report set to `true` always requires a non empty reason
XCTAssertTrue(context.viewState.isDeclineDisabled) #expect(context.viewState.isDeclineDisabled)
context.reportReason = "Test reason" 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" let reason = "Test reason"
clientProxy.roomForIdentifierClosure = { id in clientProxy.roomForIdentifierClosure = { id in
XCTAssertEqual(id, "!room:matrix.org") #expect(id == "!room:matrix.org")
let roomProxyMock = InvitedRoomProxyMock(.init(id: id)) let roomProxyMock = InvitedRoomProxyMock(.init(id: id))
roomProxyMock.rejectInvitationReturnValue = .success(()) roomProxyMock.rejectInvitationReturnValue = .success(())
return .invited(InvitedRoomProxyMock(.init(id: id))) return .invited(InvitedRoomProxyMock(.init(id: id)))
} }
clientProxy.reportRoomForIdentifierReasonClosure = { id, reasonValue in clientProxy.reportRoomForIdentifierReasonClosure = { id, reasonValue in
XCTAssertEqual(id, "!room:matrix.org") #expect(id == "!room:matrix.org")
XCTAssertEqual(reasonValue, reason) #expect(reasonValue == reason)
return .success(()) return .success(())
} }
clientProxy.ignoreUserClosure = { userId in clientProxy.ignoreUserClosure = { userId in
XCTAssertEqual(userId, "@alice:matrix.org") #expect(userId == "@alice:matrix.org")
return .success(()) return .success(())
} }
@@ -70,8 +74,8 @@ class DeclineAndBlockScreenViewModelTests: XCTestCase {
} }
context.send(viewAction: .decline) context.send(viewAction: .decline)
try await deferredAction.fulfill() try await deferredAction.fulfill()
XCTAssertTrue(clientProxy.roomForIdentifierCalled) #expect(clientProxy.roomForIdentifierCalled)
XCTAssertTrue(clientProxy.reportRoomForIdentifierReasonCalled) #expect(clientProxy.reportRoomForIdentifierReasonCalled)
XCTAssertTrue(clientProxy.ignoreUserCalled) #expect(clientProxy.ignoreUserCalled)
} }
} }

View File

@@ -7,13 +7,16 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Observation
import Testing
@MainActor @MainActor
class DeferredFulfillmentTests: XCTestCase { @Suite
struct DeferredFulfillmentTests {
private let observable = SomeObservable() 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. // Given a deferred fulfilment on a value that already matches the expected value.
let initialValue = observable.counter let initialValue = observable.counter
let deferred = deferFulfillment(observable.observe(\.counter)) { $0 == initialValue } let deferred = deferFulfillment(observable.observe(\.counter)) { $0 == initialValue }
@@ -22,35 +25,38 @@ class DeferredFulfillmentTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
} }
func testObservableWithSynchronousUpdate() async throws { @Test
func observableWithSynchronousUpdate() async throws {
// Given a deferred fulfilment for an expected value. // Given a deferred fulfilment for an expected value.
let newValue = 100 let newValue = 100
let deferred = deferFulfillment(observable.observe(\.counter)) { $0 == newValue } let deferred = deferFulfillment(observable.observe(\.counter)) { $0 == newValue }
// When that value is changed synchronously. // When that value is changed synchronously.
observable.counter = newValue observable.counter = newValue
XCTAssertEqual(observable.counter, newValue) #expect(observable.counter == newValue)
// Then the test should be fulfilled. // Then the test should be fulfilled.
try await deferred.fulfill() 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. // Given a deferred fulfilment for an expected value.
let newValue = 100 let newValue = 100
let deferred = deferFulfillment(observable.observe(\.counter)) { $0 == newValue } let deferred = deferFulfillment(observable.observe(\.counter)) { $0 == newValue }
// When that value is changed asynchronously. // When that value is changed asynchronously.
Task { try await observable.setCounter(newValue, delay: .seconds(1)) } 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. // Then the test should be fulfilled once the update has taken place.
try await deferred.fulfill() 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. // Given a deferred fulfilment for an expected value.
let finalValue = 500 let finalValue = 500
let deferred = deferFulfillment(observable.observe(\.counter)) { $0 == finalValue } 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(250, delay: .seconds(.random(in: 1.0...2.0)))
try await observable.setCounter(finalValue, 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. // Then the test should be fulfilled once the expected update has taken place.
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(observable.counter, finalValue) #expect(observable.counter == finalValue)
} }
} }

View File

@@ -1,13 +0,0 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
@testable import ElementX
import XCTest
@MainActor
class DeveloperOptionsScreenViewModelTests: XCTestCase { }

View File

@@ -8,75 +8,81 @@
import Clocks import Clocks
@testable import ElementX @testable import ElementX
import PushKit import PushKit
import XCTest import Testing
@MainActor @MainActor
class ElementCallServiceTests: XCTestCase { @Suite
var callProvider: CXProviderMock! final class ElementCallServiceTests {
var currentDate: Date! private var callProvider: CXProviderMock!
var testClock: TestClock<Duration>! private var currentDate: Date!
var pushRegistry: PKPushRegistry! private var testClock: TestClock<Duration>!
private var pushRegistry: PKPushRegistry!
private var service: ElementCallService!
var service: ElementCallService! init() {
pushRegistry = PKPushRegistry(queue: nil)
callProvider = CXProviderMock(.init())
currentDate = Date()
testClock = TestClock()
let dateProvider: () -> Date = {
self.currentDate
}
service = ElementCallService(callProvider: callProvider, timeProvider: TimeProvider(clock: testClock, now: dateProvider))
}
override func tearDown() { deinit {
callProvider = nil callProvider = nil
currentDate = nil currentDate = nil
testClock = nil testClock = nil
pushRegistry = nil pushRegistry = nil
} }
func testIncomingCall() async { @Test
setupService() func incomingCall() async {
#expect(!callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
XCTAssertFalse(callProvider.reportNewIncomingCallWithUpdateCompletionCalled) await confirmation { confirmation in
let pkPushPayloadMock = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 30)
let expectation = XCTestExpectation(description: "Call accepted") service.pushRegistry(pushRegistry, didReceiveIncomingPushWith: pkPushPayloadMock, for: .voIP) {
confirmation()
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 fulfillment(of: [expectation], timeout: 1) #expect(callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
// advance past the timeout
await testClock.advance(by: .seconds(30))
await fulfillment(of: [expectation2], timeout: 1)
} }
func testExpiredRingLifetimeIsIgnored() { @Test
setupService() func callIsTimingOut() async {
#expect(!callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
XCTAssertFalse(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) let pushPayload = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 20)
@@ -87,45 +93,31 @@ class ElementCallServiceTests: XCTestCase {
for: .voIP) { } for: .voIP) { }
sleep(20) sleep(20)
XCTAssertTrue(!callProvider.reportNewIncomingCallWithUpdateCompletionCalled) #expect(!callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
} }
func disabled_testLifetimeIsCapped() async throws { @Test
setupService() func lifetimeIsCapped() async {
await confirmation { confirmation in
let expectation = expectation(description: "Call has ended unanswered") callProvider.reportCallWithEndedAtReasonClosure = { _, _, reason in
callProvider.reportCallWithEndedAtReasonClosure = { _, _, reason in if reason == .unanswered {
if reason == .unanswered { confirmation()
expectation.fulfill() } else {
} else { Issue.record("Call should have ended as unanswered")
XCTFail("Call should have ended as unanswered") }
} }
#expect(!callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
let pushPayload = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 300)
service.pushRegistry(pushRegistry,
didReceiveIncomingPushWith: pushPayload,
for: .voIP) { }
// Advance past the max timeout but below the 300
await testClock.advance(by: .seconds(100))
} }
XCTAssertFalse(callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
let pushPayload = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 300)
service.pushRegistry(pushRegistry,
didReceiveIncomingPushWith: pushPayload,
for: .voIP) { }
// Advance past the max timeout but below the 300
await testClock.advance(by: .seconds(100))
await fulfillment(of: [expectation], timeout: 1)
}
// MARK: - Helpers
private func setupService() {
pushRegistry = PKPushRegistry(queue: nil)
callProvider = CXProviderMock(.init())
currentDate = Date()
testClock = TestClock()
let dateProvider: () -> Date = {
self.currentDate
}
service = ElementCallService(callProvider: callProvider, timeProvider: TimeProvider(clock: testClock, now: dateProvider))
} }
} }

View File

@@ -7,10 +7,11 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
final class EmojiPickerScreenViewModelTests: XCTestCase { @Suite
struct EmojiPickerScreenViewModelTests {
var timelineProxy: TimelineProxyMock! var timelineProxy: TimelineProxyMock!
var viewModel: EmojiPickerScreenViewModel! var viewModel: EmojiPickerScreenViewModel!
@@ -18,25 +19,38 @@ final class EmojiPickerScreenViewModelTests: XCTestCase {
viewModel.context viewModel.context
} }
func testToggleReaction() async throws { @Test
mutating func toggleReaction() async throws {
setupViewModel() setupViewModel()
let reaction = "👋" let reaction = "👋"
let expectation = XCTestExpectation(description: "Toggle reaction")
let deferred = deferFulfillment(viewModel.actions) { $0 == .dismiss } let deferred = deferFulfillment(viewModel.actions) { $0 == .dismiss }
timelineProxy.toggleReactionToClosure = { toggledReaction, _ in try await confirmation { confirmation in
XCTAssertEqual(toggledReaction, reaction) var toggleReactionCalled = false
expectation.fulfill() timelineProxy.toggleReactionToClosure = { toggledReaction, _ in
return .success(()) 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 // MARK: - Helpers
private func setupViewModel(selectedEmojis: Set<String> = []) { private mutating func setupViewModel(selectedEmojis: Set<String> = []) {
timelineProxy = TimelineProxyMock(.init()) timelineProxy = TimelineProxyMock(.init())
viewModel = EmojiPickerScreenViewModel(itemID: .randomEvent, viewModel = EmojiPickerScreenViewModel(itemID: .randomEvent,

View File

@@ -7,11 +7,13 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
import UIKit
@MainActor @Suite
final class EmojiProviderTests: XCTestCase { struct EmojiProviderTests {
func testWhenEmojisLoadedCategoriesAreLoadedFromLoader() async { @Test @MainActor
func emojisLoadedCategoriesAreLoadedFromLoader() async {
let item = EmojiItem(label: "test", unicode: "test", keywords: ["1", "2"], shortcodes: ["1", "2"]) let item = EmojiItem(label: "test", unicode: "test", keywords: ["1", "2"], shortcodes: ["1", "2"])
let category = EmojiCategory(id: "test", emojis: [item]) 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 emojiProvider = EmojiProvider(loader: emojiLoaderMock, appSettings: ServiceLocator.shared.settings)
let categories = await emojiProvider.categories() 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 item = EmojiItem(label: "test", unicode: "test", keywords: ["1", "2"], shortcodes: ["1", "2"])
let category = EmojiCategory(id: "test", emojis: [item]) 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 emojiProvider = EmojiProvider(loader: emojiLoaderMock, appSettings: ServiceLocator.shared.settings)
let categories = await emojiProvider.categories(searchString: "") 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 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 item2 = EmojiItem(label: "test2", unicode: "test2", keywords: ["3", "4"], shortcodes: ["3", "4"])
let categoriesForFirstLoad = [EmojiCategory(id: "test", let categoriesForFirstLoad = [EmojiCategory(id: "test",
@@ -54,10 +58,11 @@ final class EmojiProviderTests: XCTestCase {
emojiLoaderMock.categories = categoriesForSecondLoad emojiLoaderMock.categories = categoriesForSecondLoad
let categories = await emojiProvider.categories() let categories = await emojiProvider.categories()
XCTAssertEqual(categories, categoriesForFirstLoad) #expect(categories == categoriesForFirstLoad)
} }
func testWhenEmojisSearchedCorrectNumberOfCategoriesReturned() async { @Test @MainActor
func emojisSearchedCorrectNumberOfCategoriesReturned() async {
let searchString = "smile" let searchString = "smile"
var categories = [EmojiCategory]() var categories = [EmojiCategory]()
let item0WithSearchString = EmojiItem(label: "emoji0", unicode: "\(searchString)_123", keywords: ["key1", "key1"], shortcodes: ["key1", "key1"]) 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() _ = await emojiProvider.categories()
let result = await emojiProvider.categories(searchString: searchString) let result = await emojiProvider.categories(searchString: searchString)
XCTAssertEqual(result.count, 2) #expect(result.count == 2)
XCTAssertEqual(result.first?.emojis.count, 4) #expect(result.first?.emojis.count == 4)
} }
} }

View File

@@ -8,24 +8,27 @@
@testable import ElementX @testable import ElementX
import Foundation import Foundation
import XCTest import Testing
class ExpiringTaskRunnerTests: XCTestCase { @Suite
struct ExpiringTaskRunnerTests {
enum ExpiringTaskTestError: Error { enum ExpiringTaskTestError: Error {
case failed case failed
} }
func testSuccedingTask() async { @Test
func succedingTask() async throws {
let runner = ExpiringTaskRunner { let runner = ExpiringTaskRunner {
try? await Task.sleep(for: .milliseconds(300)) try? await Task.sleep(for: .milliseconds(300))
return true return true
} }
let result = try? await runner.run(timeout: .seconds(1)) let result = try await runner.run(timeout: .seconds(1))
XCTAssertEqual(result, true) #expect(result == true)
} }
func testFailingTask() async { @Test
func failingTask() async {
let runner: ExpiringTaskRunner<Result<String, ExpiringTaskTestError>> = ExpiringTaskRunner { let runner: ExpiringTaskRunner<Result<String, ExpiringTaskTestError>> = ExpiringTaskRunner {
try? await Task.sleep(for: .milliseconds(300)) try? await Task.sleep(for: .milliseconds(300))
return .failure(.failed) return .failure(.failed)
@@ -34,11 +37,12 @@ class ExpiringTaskRunnerTests: XCTestCase {
do { do {
_ = try await runner.run(timeout: .seconds(1)) _ = try await runner.run(timeout: .seconds(1))
} catch { } catch {
XCTAssertEqual(error as? ExpiringTaskTestError, ExpiringTaskTestError.failed) #expect(error as? ExpiringTaskTestError == ExpiringTaskTestError.failed)
} }
} }
func testTimeoutTask() async { @Test
func timeoutTask() async {
let runner = ExpiringTaskRunner { let runner = ExpiringTaskRunner {
try? await Task.sleep(for: .milliseconds(300)) try? await Task.sleep(for: .milliseconds(300))
return true return true
@@ -47,7 +51,7 @@ class ExpiringTaskRunnerTests: XCTestCase {
do { do {
_ = try await runner.run(timeout: .milliseconds(100)) _ = try await runner.run(timeout: .milliseconds(100))
} catch { } catch {
XCTAssertEqual(error as? ExpiringTaskRunnerError, ExpiringTaskRunnerError.timeout) #expect(error as? ExpiringTaskRunnerError == ExpiringTaskRunnerError.timeout)
} }
} }
} }

View File

@@ -7,76 +7,87 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
final class GeoURITests: XCTestCase { @Suite
func testValidPositiveCoordinates() throws { struct GeoURITests {
@Test
func validPositiveCoordinates() throws {
let string = "geo:53.9980310155285,8.25347900390625;u=10.123" let string = "geo:53.9980310155285,8.25347900390625;u=10.123"
let uri = try XCTUnwrap(GeoURI(string: string)) let uri = try #require(GeoURI(string: string))
XCTAssertEqual(uri.latitude, 53.9980310155285) #expect(uri.latitude == 53.9980310155285)
XCTAssertEqual(uri.longitude, 8.25347900390625) #expect(uri.longitude == 8.25347900390625)
XCTAssertEqual(uri.uncertainty, 10.123) #expect(uri.uncertainty == 10.123)
XCTAssertEqual(uri.string, string) #expect(uri.string == string)
} }
func testValidNegativeCoordinates() throws { @Test
func validNegativeCoordinates() throws {
let string = "geo:-53.9980310155285,-8.25347900390625;u=10" let string = "geo:-53.9980310155285,-8.25347900390625;u=10"
let uri = try XCTUnwrap(GeoURI(string: string)) let uri = try #require(GeoURI(string: string))
XCTAssertEqual(uri.latitude, -53.9980310155285) #expect(uri.latitude == -53.9980310155285)
XCTAssertEqual(uri.longitude, -8.25347900390625) #expect(uri.longitude == -8.25347900390625)
XCTAssertEqual(uri.uncertainty, 10) #expect(uri.uncertainty == 10)
XCTAssertEqual(uri.string, string) #expect(uri.string == string)
} }
func testValidMixedCoordinates() throws { @Test
func validMixedCoordinates() throws {
let string = "geo:53.9980310155285,-8.25347900390625;u=10" let string = "geo:53.9980310155285,-8.25347900390625;u=10"
let uri = try XCTUnwrap(GeoURI(string: string)) let uri = try #require(GeoURI(string: string))
XCTAssertEqual(uri.latitude, 53.9980310155285) #expect(uri.latitude == 53.9980310155285)
XCTAssertEqual(uri.longitude, -8.25347900390625) #expect(uri.longitude == -8.25347900390625)
XCTAssertEqual(uri.uncertainty, 10) #expect(uri.uncertainty == 10)
XCTAssertEqual(uri.string, string) #expect(uri.string == string)
} }
func testValidCoordinatesNoUncertainty() throws { @Test
func validCoordinatesNoUncertainty() throws {
let string = "geo:53.9980310155285,-8.25347900390625" let string = "geo:53.9980310155285,-8.25347900390625"
let uri = try XCTUnwrap(GeoURI(string: string)) let uri = try #require(GeoURI(string: string))
XCTAssertEqual(uri.latitude, 53.9980310155285) #expect(uri.latitude == 53.9980310155285)
XCTAssertEqual(uri.longitude, -8.25347900390625) #expect(uri.longitude == -8.25347900390625)
XCTAssertNil(uri.uncertainty) #expect(uri.uncertainty == nil)
XCTAssertEqual(uri.string, string) #expect(uri.string == string)
} }
func testValidIntegerCoordinates() throws { @Test
func validIntegerCoordinates() throws {
let string = "geo:53,-8;u=35" let string = "geo:53,-8;u=35"
let uri = try XCTUnwrap(GeoURI(string: string)) let uri = try #require(GeoURI(string: string))
XCTAssertEqual(uri.latitude, 53) #expect(uri.latitude == 53)
XCTAssertEqual(uri.longitude, -8) #expect(uri.longitude == -8)
XCTAssertEqual(uri.uncertainty, 35) #expect(uri.uncertainty == 35)
XCTAssertEqual(uri.string, "geo:53,-8;u=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) 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 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 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 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 let string = "geo:53.99803101552848,-8.25347900390625;u=-20" // u is negative
XCTAssertNil(GeoURI(string: string)) #expect(GeoURI(string: string) == nil)
} }
} }

View File

@@ -8,48 +8,44 @@
import Combine import Combine
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class GlobalSearchScreenViewModelTests: XCTestCase { @Suite
struct GlobalSearchScreenViewModelTests {
var viewModel: GlobalSearchScreenViewModelProtocol! var viewModel: GlobalSearchScreenViewModelProtocol!
var context: GlobalSearchScreenViewModelType.Context! var context: GlobalSearchScreenViewModelType.Context!
var cancellables = Set<AnyCancellable>()
override func setUpWithError() throws { init() {
cancellables.removeAll()
viewModel = GlobalSearchScreenViewModel(roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))), viewModel = GlobalSearchScreenViewModel(roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))),
mediaProvider: MediaProviderMock(configuration: .init())) mediaProvider: MediaProviderMock(configuration: .init()))
context = viewModel.context context = viewModel.context
} }
func testSearching() async throws { @Test
let defered = deferFulfillment(context.$viewState) { state in mutating func searching() async throws {
let deferred = deferFulfillment(context.$viewState) { state in
state.rooms.count == 1 state.rooms.count == 1
} }
context.searchQuery = "Second" context.searchQuery = "Second"
try await defered.fulfill() try await deferred.fulfill()
} }
func testRoomSelection() { @Test
let expectation = expectation(description: "Wait for confirmation") func roomSelection() async throws {
let deferred = deferFulfillment(viewModel.actions) { action in
viewModel.actions switch action {
.sink { action in case .select(let roomID):
switch action { return roomID == "2"
case .select(let roomID): default:
XCTAssertEqual(roomID, "2") return false
expectation.fulfill()
default:
break
}
} }
.store(in: &cancellables) }
context.send(viewAction: .select(roomID: "2")) context.send(viewAction: .select(roomID: "2"))
waitForExpectations(timeout: 5.0) try await deferred.fulfill()
} }
} }

View File

@@ -8,18 +8,19 @@
import Combine import Combine
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class HomeScreenRoomTests: XCTestCase { @Suite
struct HomeScreenRoomTests {
var roomSummary: RoomSummary! var roomSummary: RoomSummary!
func setupRoomSummary(isMarkedUnread: Bool, mutating func setupRoomSummary(isMarkedUnread: Bool,
unreadMessagesCount: UInt, unreadMessagesCount: UInt,
unreadMentionsCount: UInt, unreadMentionsCount: UInt,
unreadNotificationsCount: UInt, unreadNotificationsCount: UInt,
notificationMode: RoomNotificationModeProxy, notificationMode: RoomNotificationModeProxy,
hasOngoingCall: Bool) { hasOngoingCall: Bool) {
roomSummary = RoomSummary(room: .init(noHandle: .init()), roomSummary = RoomSummary(room: .init(noHandle: .init()),
id: "Test room", id: "Test room",
joinRequestType: nil, joinRequestType: nil,
@@ -44,7 +45,8 @@ class HomeScreenRoomTests: XCTestCase {
isTombstoned: false) isTombstoned: false)
} }
func testNoBadge() { @Test
mutating func noBadge() {
setupRoomSummary(isMarkedUnread: false, setupRoomSummary(isMarkedUnread: false,
unreadMessagesCount: 0, unreadMessagesCount: 0,
unreadMentionsCount: 0, unreadMentionsCount: 0,
@@ -54,14 +56,15 @@ class HomeScreenRoomTests: XCTestCase {
let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false) let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false)
XCTAssertFalse(room.isHighlighted) #expect(!room.isHighlighted)
XCTAssertFalse(room.badges.isDotShown) #expect(!room.badges.isDotShown)
XCTAssertFalse(room.badges.isCallShown) #expect(!room.badges.isCallShown)
XCTAssertFalse(room.badges.isMuteShown) #expect(!room.badges.isMuteShown)
XCTAssertFalse(room.badges.isMentionShown) #expect(!room.badges.isMentionShown)
} }
func testAllBadgesExceptMute() { @Test
mutating func allBadgesExceptMute() {
setupRoomSummary(isMarkedUnread: true, setupRoomSummary(isMarkedUnread: true,
unreadMessagesCount: 5, unreadMessagesCount: 5,
unreadMentionsCount: 5, unreadMentionsCount: 5,
@@ -71,14 +74,15 @@ class HomeScreenRoomTests: XCTestCase {
let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false) let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false)
XCTAssertTrue(room.isHighlighted) #expect(room.isHighlighted)
XCTAssertTrue(room.badges.isDotShown) #expect(room.badges.isDotShown)
XCTAssertTrue(room.badges.isCallShown) #expect(room.badges.isCallShown)
XCTAssertFalse(room.badges.isMuteShown) #expect(!room.badges.isMuteShown)
XCTAssertTrue(room.badges.isMentionShown) #expect(room.badges.isMentionShown)
} }
func testUnhighlightedDot() { @Test
mutating func unhighlightedDot() {
setupRoomSummary(isMarkedUnread: false, setupRoomSummary(isMarkedUnread: false,
unreadMessagesCount: 5, unreadMessagesCount: 5,
unreadMentionsCount: 0, unreadMentionsCount: 0,
@@ -88,14 +92,15 @@ class HomeScreenRoomTests: XCTestCase {
let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false) let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false)
XCTAssertFalse(room.isHighlighted) #expect(!room.isHighlighted)
XCTAssertTrue(room.badges.isDotShown) #expect(room.badges.isDotShown)
XCTAssertFalse(room.badges.isCallShown) #expect(!room.badges.isCallShown)
XCTAssertFalse(room.badges.isMuteShown) #expect(!room.badges.isMuteShown)
XCTAssertFalse(room.badges.isMentionShown) #expect(!room.badges.isMentionShown)
} }
func testHighlightedDot() { @Test
mutating func highlightedDot() {
setupRoomSummary(isMarkedUnread: false, setupRoomSummary(isMarkedUnread: false,
unreadMessagesCount: 0, unreadMessagesCount: 0,
unreadMentionsCount: 0, unreadMentionsCount: 0,
@@ -105,14 +110,15 @@ class HomeScreenRoomTests: XCTestCase {
let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false) let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false)
XCTAssertTrue(room.isHighlighted) #expect(room.isHighlighted)
XCTAssertTrue(room.badges.isDotShown) #expect(room.badges.isDotShown)
XCTAssertFalse(room.badges.isCallShown) #expect(!room.badges.isCallShown)
XCTAssertFalse(room.badges.isMuteShown) #expect(!room.badges.isMuteShown)
XCTAssertFalse(room.badges.isMentionShown) #expect(!room.badges.isMentionShown)
} }
func testHighlightedMentionAndDot() { @Test
mutating func highlightedMentionAndDot() {
setupRoomSummary(isMarkedUnread: false, setupRoomSummary(isMarkedUnread: false,
unreadMessagesCount: 0, unreadMessagesCount: 0,
unreadMentionsCount: 5, unreadMentionsCount: 5,
@@ -122,14 +128,15 @@ class HomeScreenRoomTests: XCTestCase {
let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false) let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false)
XCTAssertTrue(room.isHighlighted) #expect(room.isHighlighted)
XCTAssertTrue(room.badges.isDotShown) #expect(room.badges.isDotShown)
XCTAssertFalse(room.badges.isCallShown) #expect(!room.badges.isCallShown)
XCTAssertFalse(room.badges.isMuteShown) #expect(!room.badges.isMuteShown)
XCTAssertTrue(room.badges.isMentionShown) #expect(room.badges.isMentionShown)
} }
func testUnhighlightedCall() { @Test
mutating func unhighlightedCall() {
setupRoomSummary(isMarkedUnread: false, setupRoomSummary(isMarkedUnread: false,
unreadMessagesCount: 0, unreadMessagesCount: 0,
unreadMentionsCount: 0, unreadMentionsCount: 0,
@@ -139,14 +146,15 @@ class HomeScreenRoomTests: XCTestCase {
let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false) let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false)
XCTAssertFalse(room.isHighlighted) #expect(!room.isHighlighted)
XCTAssertFalse(room.badges.isDotShown) #expect(!room.badges.isDotShown)
XCTAssertTrue(room.badges.isCallShown) #expect(room.badges.isCallShown)
XCTAssertFalse(room.badges.isMuteShown) #expect(!room.badges.isMuteShown)
XCTAssertFalse(room.badges.isMentionShown) #expect(!room.badges.isMentionShown)
} }
func testMentionAndKeywordsUnhighlightedDot() { @Test
mutating func mentionAndKeywordsUnhighlightedDot() {
setupRoomSummary(isMarkedUnread: false, setupRoomSummary(isMarkedUnread: false,
unreadMessagesCount: 10, unreadMessagesCount: 10,
unreadMentionsCount: 0, unreadMentionsCount: 0,
@@ -156,14 +164,15 @@ class HomeScreenRoomTests: XCTestCase {
let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false) let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false)
XCTAssertFalse(room.isHighlighted) #expect(!room.isHighlighted)
XCTAssertTrue(room.badges.isDotShown) #expect(room.badges.isDotShown)
XCTAssertFalse(room.badges.isCallShown) #expect(!room.badges.isCallShown)
XCTAssertFalse(room.badges.isMuteShown) #expect(!room.badges.isMuteShown)
XCTAssertFalse(room.badges.isMentionShown) #expect(!room.badges.isMentionShown)
} }
func testMentionAndKeywordsUnhighlightedDotHidden() { @Test
mutating func mentionAndKeywordsUnhighlightedDotHidden() {
setupRoomSummary(isMarkedUnread: false, setupRoomSummary(isMarkedUnread: false,
unreadMessagesCount: 10, unreadMessagesCount: 10,
unreadMentionsCount: 0, unreadMentionsCount: 0,
@@ -173,16 +182,17 @@ class HomeScreenRoomTests: XCTestCase {
let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: true) let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: true)
XCTAssertFalse(room.isHighlighted) #expect(!room.isHighlighted)
XCTAssertFalse(room.badges.isDotShown) #expect(!room.badges.isDotShown)
XCTAssertFalse(room.badges.isCallShown) #expect(!room.badges.isCallShown)
XCTAssertFalse(room.badges.isMuteShown) #expect(!room.badges.isMuteShown)
XCTAssertFalse(room.badges.isMentionShown) #expect(!room.badges.isMentionShown)
} }
// MARK: - Mark unread // MARK: - Mark unread
func testMarkedUnreadDot() { @Test
mutating func markedUnreadDot() {
setupRoomSummary(isMarkedUnread: true, setupRoomSummary(isMarkedUnread: true,
unreadMessagesCount: 0, unreadMessagesCount: 0,
unreadMentionsCount: 0, unreadMentionsCount: 0,
@@ -192,14 +202,15 @@ class HomeScreenRoomTests: XCTestCase {
let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false) let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false)
XCTAssertTrue(room.isHighlighted) #expect(room.isHighlighted)
XCTAssertTrue(room.badges.isDotShown) #expect(room.badges.isDotShown)
XCTAssertFalse(room.badges.isCallShown) #expect(!room.badges.isCallShown)
XCTAssertFalse(room.badges.isMuteShown) #expect(!room.badges.isMuteShown)
XCTAssertFalse(room.badges.isMentionShown) #expect(!room.badges.isMentionShown)
} }
func testMarkedUnreadDotAndMention() { @Test
mutating func markedUnreadDotAndMention() {
setupRoomSummary(isMarkedUnread: true, setupRoomSummary(isMarkedUnread: true,
unreadMessagesCount: 0, unreadMessagesCount: 0,
unreadMentionsCount: 5, unreadMentionsCount: 5,
@@ -209,14 +220,15 @@ class HomeScreenRoomTests: XCTestCase {
let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false) let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false)
XCTAssertTrue(room.isHighlighted) #expect(room.isHighlighted)
XCTAssertTrue(room.badges.isDotShown) #expect(room.badges.isDotShown)
XCTAssertFalse(room.badges.isCallShown) #expect(!room.badges.isCallShown)
XCTAssertFalse(room.badges.isMuteShown) #expect(!room.badges.isMuteShown)
XCTAssertTrue(room.badges.isMentionShown) #expect(room.badges.isMentionShown)
} }
func testMarkedUnreadMuteDotAndCall() { @Test
mutating func markedUnreadMuteDotAndCall() {
setupRoomSummary(isMarkedUnread: true, setupRoomSummary(isMarkedUnread: true,
unreadMessagesCount: 5, unreadMessagesCount: 5,
unreadMentionsCount: 5, unreadMentionsCount: 5,
@@ -226,10 +238,10 @@ class HomeScreenRoomTests: XCTestCase {
let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false) let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false)
XCTAssertTrue(room.isHighlighted) #expect(room.isHighlighted)
XCTAssertTrue(room.badges.isDotShown) #expect(room.badges.isDotShown)
XCTAssertTrue(room.badges.isCallShown) #expect(room.badges.isCallShown)
XCTAssertTrue(room.badges.isMuteShown) #expect(room.badges.isMuteShown)
XCTAssertFalse(room.badges.isMentionShown) #expect(!room.badges.isMentionShown)
} }
} }

View File

@@ -8,10 +8,11 @@
import Combine import Combine
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class HomeScreenViewModelTests: XCTestCase { @Suite
final class HomeScreenViewModelTests {
var viewModel: HomeScreenViewModelProtocol! var viewModel: HomeScreenViewModelProtocol!
var context: HomeScreenViewModelType.Context! { var context: HomeScreenViewModelType.Context! {
viewModel.context viewModel.context
@@ -24,19 +25,18 @@ class HomeScreenViewModelTests: XCTestCase {
var cancellables = Set<AnyCancellable>() var cancellables = Set<AnyCancellable>()
override func setUp() { init() {
cancellables.removeAll()
AppSettings.resetAllSettings() AppSettings.resetAllSettings()
appSettings = AppSettings() appSettings = AppSettings()
ServiceLocator.shared.register(appSettings: appSettings) ServiceLocator.shared.register(appSettings: appSettings)
} }
override func tearDown() { deinit {
AppSettings.resetAllSettings() AppSettings.resetAllSettings()
} }
func testSelectRoom() async { @Test
func selectRoom() async {
setupViewModel() setupViewModel()
let mockRoomID = "mock_room_id" let mockRoomID = "mock_room_id"
@@ -57,11 +57,12 @@ class HomeScreenViewModelTests: XCTestCase {
context.send(viewAction: .selectRoom(roomIdentifier: mockRoomID)) context.send(viewAction: .selectRoom(roomIdentifier: mockRoomID))
await Task.yield() await Task.yield()
XCTAssert(correctResult) #expect(correctResult)
XCTAssertEqual(mockRoomID, selectedRoomID) #expect(mockRoomID == selectedRoomID)
} }
func testTapUserAvatar() async { @Test
func tapUserAvatar() async {
setupViewModel() setupViewModel()
var correctResult = false var correctResult = false
@@ -79,10 +80,11 @@ class HomeScreenViewModelTests: XCTestCase {
context.send(viewAction: .showSettings) context.send(viewAction: .showSettings)
await Task.yield() await Task.yield()
XCTAssert(correctResult) #expect(correctResult)
} }
func testLeaveRoomAlert() async throws { @Test
func leaveRoomAlert() async throws {
setupViewModel() setupViewModel()
let mockRoomID = "1" let mockRoomID = "1"
@@ -97,10 +99,11 @@ class HomeScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(context.leaveRoomAlertItem?.roomID, mockRoomID) #expect(context.leaveRoomAlertItem?.roomID == mockRoomID)
} }
func testLeaveRoomError() async throws { @Test
func leaveRoomError() async throws {
setupViewModel() setupViewModel()
let mockRoomID = "1" let mockRoomID = "1"
@@ -117,38 +120,34 @@ class HomeScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertNotNil(context.alertInfo) #expect(context.alertInfo != nil)
} }
func testLeaveRoomSuccess() async { @Test
func leaveRoomSuccess() async throws {
setupViewModel() setupViewModel()
let mockRoomID = "1" 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")) let room = JoinedRoomProxyMock(.init(id: mockRoomID, name: "Some room"))
room.leaveRoomClosure = { .success(()) } room.leaveRoomClosure = { .success(()) }
clientProxy.roomForIdentifierClosure = { _ in .joined(room) } 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)) context.send(viewAction: .confirmLeaveRoom(roomIdentifier: mockRoomID))
await fulfillment(of: [expectation]) try await deferred.fulfill()
XCTAssertNil(context.alertInfo) #expect(context.alertInfo == nil)
XCTAssertTrue(correctResult)
} }
func testShowRoomDetails() async { @Test
func showRoomDetails() async {
setupViewModel() setupViewModel()
let mockRoomID = "1" let mockRoomID = "1"
@@ -165,45 +164,49 @@ class HomeScreenViewModelTests: XCTestCase {
.store(in: &cancellables) .store(in: &cancellables)
context.send(viewAction: .showRoomDetails(roomIdentifier: mockRoomID)) context.send(viewAction: .showRoomDetails(roomIdentifier: mockRoomID))
await Task.yield() await Task.yield()
XCTAssertNil(context.alertInfo) #expect(context.alertInfo == nil)
XCTAssertTrue(correctResult) #expect(correctResult)
} }
func testFilters() async throws { @Test
func filters() async throws {
setupViewModel() setupViewModel()
context.filtersState.activateFilter(.people) context.filtersState.activateFilter(.people)
try await Task.sleep(for: .milliseconds(100)) try await Task.sleep(for: .milliseconds(100))
XCTAssertEqual(roomSummaryProvider.roomListPublisher.value.count, 2) #expect(roomSummaryProvider.roomListPublisher.value.count == 2)
XCTAssertEqual(roomSummaryProvider.roomListPublisher.value.first?.name, "Foundation and Earth") #expect(roomSummaryProvider.roomListPublisher.value.first?.name == "Foundation and Earth")
} }
func testSearch() async throws { @Test
func search() async throws {
setupViewModel() setupViewModel()
context.isSearchFieldFocused = true context.isSearchFieldFocused = true
context.searchQuery = "lude to Found" context.searchQuery = "lude to Found"
try await Task.sleep(for: .milliseconds(100)) try await Task.sleep(for: .milliseconds(100))
XCTAssertEqual(roomSummaryProvider.roomListPublisher.value.first?.name, "Prelude to Foundation") #expect(roomSummaryProvider.roomListPublisher.value.first?.name == "Prelude to Foundation")
XCTAssertEqual(roomSummaryProvider.roomListPublisher.value.count, 1) #expect(roomSummaryProvider.roomListPublisher.value.count == 1)
} }
func testFiltersEmptyState() async throws { @Test
func filtersEmptyState() async throws {
setupViewModel() setupViewModel()
context.filtersState.activateFilter(.people) context.filtersState.activateFilter(.people)
context.filtersState.activateFilter(.favourites) context.filtersState.activateFilter(.favourites)
try await Task.sleep(for: .milliseconds(100)) try await Task.sleep(for: .milliseconds(100))
XCTAssertTrue(context.viewState.shouldShowEmptyFilterState) #expect(context.viewState.shouldShowEmptyFilterState)
context.isSearchFieldFocused = true 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. // Given a view model without a visible security banner.
let securityStateStateSubject = CurrentValueSubject<SessionSecurityState, Never>(.init(verificationState: .verified, recoveryState: .unknown)) let securityStateStateSubject = CurrentValueSubject<SessionSecurityState, Never>(.init(verificationState: .verified, recoveryState: .unknown))
setupViewModel(securityStatePublisher: securityStateStateSubject.asCurrentValuePublisher()) setupViewModel(securityStatePublisher: securityStateStateSubject.asCurrentValuePublisher())
XCTAssertEqual(context.viewState.securityBannerMode, .none) #expect(context.viewState.securityBannerMode == .none)
// When the recovery state comes through as disabled. // When the recovery state comes through as disabled.
var deferred = deferFulfillment(context.$viewState) { $0.requiresExtraAccountSetup == true } var deferred = deferFulfillment(context.$viewState) { $0.requiresExtraAccountSetup == true }
@@ -211,7 +214,7 @@ class HomeScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
// Then the banner should be shown to set up recovery. // 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. // When the recovery is enabled.
deferred = deferFulfillment(context.$viewState) { $0.requiresExtraAccountSetup == false } deferred = deferFulfillment(context.$viewState) { $0.requiresExtraAccountSetup == false }
@@ -219,10 +222,11 @@ class HomeScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
// Then the banner should no longer be shown. // 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. // Given a view model with the setup recovery banner shown.
let securityStateStateSubject = CurrentValueSubject<SessionSecurityState, Never>(.init(verificationState: .verified, recoveryState: .unknown)) let securityStateStateSubject = CurrentValueSubject<SessionSecurityState, Never>(.init(verificationState: .verified, recoveryState: .unknown))
setupViewModel(securityStatePublisher: securityStateStateSubject.asCurrentValuePublisher()) setupViewModel(securityStatePublisher: securityStateStateSubject.asCurrentValuePublisher())
@@ -238,16 +242,17 @@ class HomeScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
// And when the recovery state comes through a second time the banner should still not be shown. // 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)) securityStateStateSubject.send(.init(verificationState: .verified, recoveryState: .disabled))
try await failure.fulfill() try await failure.fulfill()
} }
func testOutOfSyncRecoveryBannerState() async throws { @Test
func outOfSyncRecoveryBannerState() async throws {
// Given a view model without a visible security banner. // Given a view model without a visible security banner.
let securityStateStateSubject = CurrentValueSubject<SessionSecurityState, Never>(.init(verificationState: .verified, recoveryState: .unknown)) let securityStateStateSubject = CurrentValueSubject<SessionSecurityState, Never>(.init(verificationState: .verified, recoveryState: .unknown))
setupViewModel(securityStatePublisher: securityStateStateSubject.asCurrentValuePublisher()) setupViewModel(securityStatePublisher: securityStateStateSubject.asCurrentValuePublisher())
XCTAssertEqual(context.viewState.securityBannerMode, .none) #expect(context.viewState.securityBannerMode == .none)
// When the recovery state comes through as incomplete. // When the recovery state comes through as incomplete.
var deferred = deferFulfillment(context.$viewState) { $0.requiresExtraAccountSetup == true } var deferred = deferFulfillment(context.$viewState) { $0.requiresExtraAccountSetup == true }
@@ -255,7 +260,7 @@ class HomeScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
// Then the banner should be shown for out of sync recovery. // 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. // When the recovery is enabled.
deferred = deferFulfillment(context.$viewState) { $0.requiresExtraAccountSetup == false } deferred = deferFulfillment(context.$viewState) { $0.requiresExtraAccountSetup == false }
@@ -263,16 +268,17 @@ class HomeScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
// Then the banner should no longer be shown. // 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) setupViewModel(invites: .rooms)
var invites = context.viewState.rooms.invites var invites = context.viewState.rooms.invites
XCTAssertEqual(invites.count, 2) #expect(invites.count == 2)
for invite in invites { for invite in invites {
XCTAssertTrue(invite.badges.isDotShown) #expect(invite.badges.isDotShown)
} }
let deferred = deferFulfillment(context.$viewState) { state in let deferred = deferFulfillment(context.$viewState) { state in
@@ -285,31 +291,33 @@ class HomeScreenViewModelTests: XCTestCase {
invites = context.viewState.rooms.invites invites = context.viewState.rooms.invites
for invite in 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) setupViewModel(invites: .rooms)
let invitedRoomIDs = context.viewState.rooms.invites.compactMap(\.roomID) let invitedRoomIDs = context.viewState.rooms.invites.compactMap(\.roomID)
appSettings.seenInvites = Set(invitedRoomIDs) appSettings.seenInvites = Set(invitedRoomIDs)
XCTAssertEqual(invitedRoomIDs.count, 2) #expect(invitedRoomIDs.count == 2)
let deferred = deferFulfillment(viewModel.actions) { $0 == .presentRoom(roomIdentifier: invitedRoomIDs[0]) } let deferred = deferFulfillment(viewModel.actions) { $0 == .presentRoom(roomIdentifier: invitedRoomIDs[0]) }
context.send(viewAction: .acceptInvite(roomIdentifier: invitedRoomIDs[0])) context.send(viewAction: .acceptInvite(roomIdentifier: invitedRoomIDs[0]))
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(appSettings.seenInvites, [invitedRoomIDs[1]]) #expect(appSettings.seenInvites == [invitedRoomIDs[1]])
XCTAssertFalse(notificationManager.removeDeliveredMessageNotificationsForCalled, "The notification will be dismissed when opening the room.") #expect(!notificationManager.removeDeliveredMessageNotificationsForCalled, "The notification will be dismissed when opening the room.")
} }
func testAcceptSpaceInvite() async throws { @Test
func acceptSpaceInvite() async throws {
setupViewModel(invites: .spaces) setupViewModel(invites: .spaces)
let invitedRoomIDs = context.viewState.rooms.invites.compactMap(\.roomID) let invitedRoomIDs = context.viewState.rooms.invites.compactMap(\.roomID)
appSettings.seenInvites = Set(invitedRoomIDs) appSettings.seenInvites = Set(invitedRoomIDs)
XCTAssertEqual(invitedRoomIDs.count, 2) #expect(invitedRoomIDs.count == 2)
let deferred = deferFulfillment(viewModel.actions) { let deferred = deferFulfillment(viewModel.actions) {
$0 == .presentSpace(SpaceRoomListProxyMock(.init(spaceServiceRoom: SpaceServiceRoom.mock(id: invitedRoomIDs[0], isSpace: true)))) $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])) context.send(viewAction: .acceptInvite(roomIdentifier: invitedRoomIDs[0]))
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(appSettings.seenInvites, [invitedRoomIDs[1]]) #expect(appSettings.seenInvites == [invitedRoomIDs[1]])
XCTAssertFalse(notificationManager.removeDeliveredMessageNotificationsForCalled, "The notification will be dismissed when opening the room.") #expect(!notificationManager.removeDeliveredMessageNotificationsForCalled, "The notification will be dismissed when opening the room.")
} }
func testDeclineInvite() async throws { @Test
func declineInvite() async throws {
setupViewModel(invites: .rooms) setupViewModel(invites: .rooms)
let invitedRoomIDs = context.viewState.rooms.invites.compactMap(\.roomID) let invitedRoomIDs = context.viewState.rooms.invites.compactMap(\.roomID)
appSettings.seenInvites = Set(invitedRoomIDs) appSettings.seenInvites = Set(invitedRoomIDs)
XCTAssertEqual(invitedRoomIDs.count, 2) #expect(invitedRoomIDs.count == 2)
let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil } let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
context.send(viewAction: .declineInvite(roomIdentifier: invitedRoomIDs[0])) context.send(viewAction: .declineInvite(roomIdentifier: invitedRoomIDs[0]))
try await deferred.fulfill() try await deferred.fulfill()
let rejectExpectation = expectation(description: "Expected rejectInvitation to be called.") var rejectCalled = false
clientProxy.roomForIdentifierClosure = { _ in clientProxy.roomForIdentifierClosure = { _ in
let roomProxy = InvitedRoomProxyMock(.init()) let roomProxy = InvitedRoomProxyMock(.init())
roomProxy.rejectInvitationClosure = { roomProxy.rejectInvitationClosure = {
rejectExpectation.fulfill() rejectCalled = true
return .success(()) return .success(())
} }
return .invited(roomProxy) return .invited(roomProxy)
} }
context.viewState.bindings.alertInfo?.verticalButtons?[0].action?() context.viewState.bindings.alertInfo?.verticalButtons?[0].action?()
await fulfillment(of: [rejectExpectation], timeout: 1.0)
XCTAssertEqual(appSettings.seenInvites, [invitedRoomIDs[1]]) // Wait for the async action to complete
XCTAssertTrue(notificationManager.removeDeliveredMessageNotificationsForCalled) try await Task.sleep(for: .milliseconds(100))
XCTAssertEqual(notificationManager.removeDeliveredMessageNotificationsForReceivedInvocations, [invitedRoomIDs[0]]) #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) setupViewModel(invites: .rooms)
let invitedRoomIDs = context.viewState.rooms.invites.compactMap(\.roomID) let invitedRoomIDs = context.viewState.rooms.invites.compactMap(\.roomID)
appSettings.seenInvites = Set(invitedRoomIDs) appSettings.seenInvites = Set(invitedRoomIDs)
XCTAssertEqual(invitedRoomIDs.count, 2) #expect(invitedRoomIDs.count == 2)
let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil } let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
context.send(viewAction: .declineInvite(roomIdentifier: invitedRoomIDs[0])) context.send(viewAction: .declineInvite(roomIdentifier: invitedRoomIDs[0]))
@@ -364,17 +377,18 @@ class HomeScreenViewModelTests: XCTestCase {
try await deferredAction.fulfill() try await deferredAction.fulfill()
} }
func testNewSoundBanner() { @Test
func newSoundBanner() {
appSettings.hasSeenNewSoundBanner = false appSettings.hasSeenNewSoundBanner = false
setupViewModel() setupViewModel()
XCTAssertTrue(context.viewState.shouldShowBanner) #expect(context.viewState.shouldShowBanner)
XCTAssertTrue(context.viewState.shouldShowNewSoundBanner) #expect(context.viewState.shouldShowNewSoundBanner)
context.send(viewAction: .dismissNewSoundBanner) context.send(viewAction: .dismissNewSoundBanner)
XCTAssertFalse(context.viewState.shouldShowBanner) #expect(!context.viewState.shouldShowBanner)
XCTAssertFalse(context.viewState.shouldShowNewSoundBanner) #expect(!context.viewState.shouldShowNewSoundBanner)
XCTAssertTrue(appSettings.hasSeenNewSoundBanner) #expect(appSettings.hasSeenNewSoundBanner)
} }
// MARK: - Helpers // MARK: - Helpers
@@ -382,6 +396,8 @@ class HomeScreenViewModelTests: XCTestCase {
enum InviteType { case rooms, spaces } enum InviteType { case rooms, spaces }
private func setupViewModel(securityStatePublisher: CurrentValuePublisher<SessionSecurityState, Never>? = nil, invites: InviteType? = nil) { private func setupViewModel(securityStatePublisher: CurrentValuePublisher<SessionSecurityState, Never>? = nil, invites: InviteType? = nil) {
cancellables.removeAll()
var rooms: [RoomSummary] = .mockRooms var rooms: [RoomSummary] = .mockRooms
switch invites { switch invites {

View File

@@ -8,10 +8,11 @@
import Combine import Combine
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class InviteUsersScreenViewModelTests: XCTestCase { @Suite
struct InviteUsersScreenViewModelTests {
var viewModel: InviteUsersScreenViewModelProtocol! var viewModel: InviteUsersScreenViewModelProtocol!
var userDiscoveryService: UserDiscoveryServiceMock! var userDiscoveryService: UserDiscoveryServiceMock!
@@ -19,44 +20,48 @@ class InviteUsersScreenViewModelTests: XCTestCase {
viewModel.context viewModel.context
} }
func testSelectUser() { @Test
mutating func selectUser() {
let roomProxy = JoinedRoomProxyMock(.init(name: "newroom", members: [])) let roomProxy = JoinedRoomProxyMock(.init(name: "newroom", members: []))
roomProxy.inviteUserIDReturnValue = .success(()) roomProxy.inviteUserIDReturnValue = .success(())
setupViewModel(roomProxy: roomProxy, isSkippable: true) setupViewModel(roomProxy: roomProxy, isSkippable: true)
XCTAssertTrue(context.viewState.selectedUsers.isEmpty) #expect(context.viewState.selectedUsers.isEmpty)
context.send(viewAction: .toggleUser(.mockAlice)) context.send(viewAction: .toggleUser(.mockAlice))
XCTAssertTrue(context.viewState.selectedUsers.count == 1) #expect(context.viewState.selectedUsers.count == 1)
XCTAssertEqual(context.viewState.selectedUsers.first?.userID, UserProfileProxy.mockAlice.userID) #expect(context.viewState.selectedUsers.first?.userID == UserProfileProxy.mockAlice.userID)
} }
func testReselectUser() { @Test
mutating func reselectUser() {
let roomProxy = JoinedRoomProxyMock(.init(name: "newroom", members: [])) let roomProxy = JoinedRoomProxyMock(.init(name: "newroom", members: []))
roomProxy.inviteUserIDReturnValue = .success(()) roomProxy.inviteUserIDReturnValue = .success(())
setupViewModel(roomProxy: roomProxy, isSkippable: true) setupViewModel(roomProxy: roomProxy, isSkippable: true)
XCTAssertTrue(context.viewState.selectedUsers.isEmpty) #expect(context.viewState.selectedUsers.isEmpty)
context.send(viewAction: .toggleUser(.mockAlice)) context.send(viewAction: .toggleUser(.mockAlice))
XCTAssertEqual(context.viewState.selectedUsers.count, 1) #expect(context.viewState.selectedUsers.count == 1)
XCTAssertEqual(context.viewState.selectedUsers.first?.userID, UserProfileProxy.mockAlice.userID) #expect(context.viewState.selectedUsers.first?.userID == UserProfileProxy.mockAlice.userID)
context.send(viewAction: .toggleUser(.mockAlice)) 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: [])) let roomProxy = JoinedRoomProxyMock(.init(name: "newroom", members: []))
roomProxy.inviteUserIDReturnValue = .success(()) roomProxy.inviteUserIDReturnValue = .success(())
setupViewModel(roomProxy: roomProxy, isSkippable: true) setupViewModel(roomProxy: roomProxy, isSkippable: true)
XCTAssertTrue(context.viewState.selectedUsers.isEmpty) #expect(context.viewState.selectedUsers.isEmpty)
context.send(viewAction: .toggleUser(.mockAlice)) context.send(viewAction: .toggleUser(.mockAlice))
XCTAssertEqual(context.viewState.selectedUsers.count, 1) #expect(context.viewState.selectedUsers.count == 1)
XCTAssertEqual(context.viewState.selectedUsers.first?.userID, UserProfileProxy.mockAlice.userID) #expect(context.viewState.selectedUsers.first?.userID == UserProfileProxy.mockAlice.userID)
context.send(viewAction: .toggleUser(.mockAlice)) 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 mockedMembers: [RoomMemberProxyMock] = [.mockAlice, .mockBob]
let roomProxy = JoinedRoomProxyMock(.init(name: "test", members: mockedMembers)) let roomProxy = JoinedRoomProxyMock(.init(name: "test", members: mockedMembers))
roomProxy.inviteUserIDReturnValue = .success(()) roomProxy.inviteUserIDReturnValue = .success(())
@@ -80,10 +85,10 @@ class InviteUsersScreenViewModelTests: XCTestCase {
context.send(viewAction: .proceed) context.send(viewAction: .proceed)
try await deferredAction.fulfill() 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 = UserDiscoveryServiceMock()
userDiscoveryService.searchProfilesWithReturnValue = .success([]) userDiscoveryService.searchProfilesWithReturnValue = .success([])
let viewModel = InviteUsersScreenViewModel(userSession: UserSessionMock(.init()), let viewModel = InviteUsersScreenViewModel(userSession: UserSessionMock(.init()),

View File

@@ -7,22 +7,25 @@
// //
@testable import ElementX @testable import ElementX
import Foundation
import KeychainAccess import KeychainAccess
import XCTest import Testing
class KeychainControllerTests: XCTestCase { @Suite
var keychain: KeychainController! struct KeychainControllerTests {
var keychain: KeychainController
override func setUp() { init() {
keychain = KeychainController(service: .tests, keychain = KeychainController(service: .tests,
accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier) accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
keychain.removeAllRestorationTokens() keychain.removeAllRestorationTokens()
keychain.resetSecrets() keychain.resetSecrets()
} }
func testAddRestorationToken() { @Test
func addRestorationToken() {
// Given an empty keychain. // 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. // When adding an restoration token.
let username = "@test:example.com" let username = "@test:example.com"
@@ -39,10 +42,11 @@ class KeychainControllerTests: XCTestCase {
keychain.setRestorationToken(restorationToken, forUsername: username) keychain.setRestorationToken(restorationToken, forUsername: username)
// Then the restoration token should be stored in the keychain. // 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. // Given a keychain with a stored restoration token.
let username = "@test:example.com" let username = "@test:example.com"
let restorationToken = RestorationToken(session: .init(accessToken: "accessToken", let restorationToken = RestorationToken(session: .init(accessToken: "accessToken",
@@ -56,18 +60,19 @@ class KeychainControllerTests: XCTestCase {
passphrase: "passphrase", passphrase: "passphrase",
pusherNotificationClientIdentifier: "pusherClientID") pusherNotificationClientIdentifier: "pusherClientID")
keychain.setRestorationToken(restorationToken, forUsername: username) keychain.setRestorationToken(restorationToken, forUsername: username)
XCTAssertEqual(keychain.restorationTokens().count, 1, "The keychain should have 1 restoration token.") #expect(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.restorationTokenForUsername(username) == restorationToken, "The initial restoration token should match the value that was stored.")
// When deleting the restoration token. // When deleting the restoration token.
keychain.removeRestorationTokenForUsername(username) keychain.removeRestorationTokenForUsername(username)
// Then the keychain should be empty. // 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.")
XCTAssertNil(keychain.restorationTokenForUsername(username), "There restoration token should not be returned after removal.") #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. // Given a keychain with 5 stored restoration tokens.
for index in 0..<5 { for index in 0..<5 {
let restorationToken = RestorationToken(session: .init(accessToken: "accessToken", let restorationToken = RestorationToken(session: .init(accessToken: "accessToken",
@@ -82,16 +87,17 @@ class KeychainControllerTests: XCTestCase {
pusherNotificationClientIdentifier: "pusherClientID") pusherNotificationClientIdentifier: "pusherClientID")
keychain.setRestorationToken(restorationToken, forUsername: "@test\(index):example.com") 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. // When deleting all of the restoration tokens.
keychain.removeAllRestorationTokens() keychain.removeAllRestorationTokens()
// Then the keychain should be empty. // 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. // Given a keychain with 5 stored restoration tokens.
for index in 0..<5 { for index in 0..<5 {
let restorationToken = RestorationToken(session: .init(accessToken: "accessToken", let restorationToken = RestorationToken(session: .init(accessToken: "accessToken",
@@ -106,137 +112,140 @@ class KeychainControllerTests: XCTestCase {
pusherNotificationClientIdentifier: "pusherClientID") pusherNotificationClientIdentifier: "pusherClientID")
keychain.setRestorationToken(restorationToken, forUsername: "@test\(index):example.com") 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. // When deleting one of the restoration tokens.
keychain.removeRestorationTokenForUsername("@test2:example.com") keychain.removeRestorationTokenForUsername("@test2:example.com")
// Then the other 4 items should remain untouched. // Then the other 4 items should remain untouched.
XCTAssertEqual(keychain.restorationTokens().count, 4, "The keychain have 4 remaining restoration tokens.") #expect(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.") #expect(keychain.restorationTokenForUsername("@test0:example.com") != nil, "The restoration token should not have been deleted.")
XCTAssertNotNil(keychain.restorationTokenForUsername("@test1:example.com"), "The restoration token should not have been deleted.") #expect(keychain.restorationTokenForUsername("@test1:example.com") != nil, "The restoration token should not have been deleted.")
XCTAssertNil(keychain.restorationTokenForUsername("@test2:example.com"), "The restoration token should have been deleted.") #expect(keychain.restorationTokenForUsername("@test2:example.com") == nil, "The restoration token should have been deleted.")
XCTAssertNotNil(keychain.restorationTokenForUsername("@test3:example.com"), "The restoration token should not have been deleted.") #expect(keychain.restorationTokenForUsername("@test3:example.com") != nil, "The restoration token should not have been deleted.")
XCTAssertNotNil(keychain.restorationTokenForUsername("@test4:example.com"), "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. // Given a keychain with an unsupported restoration token with a sliding sync proxy URL value.
let underlyingKeychain = Keychain(service: KeychainControllerService.tests.restorationTokenID, let underlyingKeychain = Keychain(service: KeychainControllerService.tests.restorationTokenID,
accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier) accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
// Note: We assert with this underlying keychain's keys as keychain.restorationTokens() triggers the deletion that we're testing. // 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",
let unsupportedToken = RestorationTokenV4(session: SessionV1(accessToken: "1234", refreshToken: nil,
refreshToken: nil, userId: "@test:example.com",
userId: "@test:example.com", deviceId: "D3V1C3",
deviceId: "D3V1C3", homeserverUrl: "https://matrix.example.com",
homeserverUrl: "https://matrix.example.com", oidcData: nil,
oidcData: nil, slidingSyncVersion: .proxy(url: "https://sync.example.com")),
slidingSyncVersion: .proxy(url: "https://sync.example.com")), sessionDirectory: .sessionsBaseDirectory.appending(component: UUID().uuidString),
sessionDirectory: .sessionsBaseDirectory.appending(component: UUID().uuidString), passphrase: "passphrase",
passphrase: "passphrase", pusherNotificationClientIdentifier: "pusherClientID")
pusherNotificationClientIdentifier: "pusherClientID") let tokenData = try JSONEncoder().encode(unsupportedToken)
let tokenData = try JSONEncoder().encode(unsupportedToken) try underlyingKeychain.set(tokenData, key: "@test:example.com")
try underlyingKeychain.set(tokenData, key: "@test:example.com") #expect(underlyingKeychain.allKeys().count == 1)
XCTAssertEqual(underlyingKeychain.allKeys().count, 1)
} catch {
XCTFail("Failed storing user restore token with error: \(error)")
}
// When attempting to retrieve the unsupported token. // When attempting to retrieve the unsupported token.
let retrievedToken = keychain.restorationTokenForUsername("@test:example.com") let retrievedToken = keychain.restorationTokenForUsername("@test:example.com")
// Then nothing should be returned and the restoration token should be automatically removed. // Then nothing should be returned and the restoration token should be automatically removed.
XCTAssertNil(retrievedToken, "The token should not be decoded.") #expect(retrievedToken == nil, "The token should not be decoded.")
XCTAssertTrue(underlyingKeychain.allKeys().isEmpty, "The keychain should be empty again.") #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. // Given a keychain without a PIN code set.
try XCTAssertFalse(keychain.containsPINCode(), "A new keychain shouldn't contain a PIN code.") #expect(try !keychain.containsPINCode(), "A new keychain shouldn't contain a PIN code.")
XCTAssertNil(keychain.pinCode(), "A new keychain shouldn't return a PIN code.") #expect(keychain.pinCode() == nil, "A new keychain shouldn't return a PIN code.")
// When setting a PIN code. // When setting a PIN code.
try keychain.setPINCode("0000") try keychain.setPINCode("0000")
// Then the PIN code should be stored. // Then the PIN code should be stored.
try XCTAssertTrue(keychain.containsPINCode(), "The keychain should contain the PIN code.") #expect(try keychain.containsPINCode(), "The keychain should contain the PIN code.")
XCTAssertEqual(keychain.pinCode(), "0000", "The stored PIN code should match what was set.") #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. // Given a keychain with a PIN code already set.
try keychain.setPINCode("0000") try keychain.setPINCode("0000")
try XCTAssertTrue(keychain.containsPINCode(), "The keychain should contain the PIN code.") #expect(try keychain.containsPINCode(), "The keychain should contain the PIN code.")
XCTAssertEqual(keychain.pinCode(), "0000", "The stored PIN code should match what was set.") #expect(keychain.pinCode() == "0000", "The stored PIN code should match what was set.")
// When setting a different PIN code. // When setting a different PIN code.
try keychain.setPINCode("1234") try keychain.setPINCode("1234")
// Then the PIN code should be updated. // Then the PIN code should be updated.
try XCTAssertTrue(keychain.containsPINCode(), "The keychain should still contain the PIN code.") #expect(try keychain.containsPINCode(), "The keychain should still contain the PIN code.")
XCTAssertEqual(keychain.pinCode(), "1234", "The stored PIN code should match the new value.") #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. // Given a keychain with a PIN code already set.
try keychain.setPINCode("0000") try keychain.setPINCode("0000")
try XCTAssertTrue(keychain.containsPINCode(), "The keychain should contain the PIN code.") #expect(try keychain.containsPINCode(), "The keychain should contain the PIN code.")
XCTAssertEqual(keychain.pinCode(), "0000", "The stored PIN code should match what was set.") #expect(keychain.pinCode() == "0000", "The stored PIN code should match what was set.")
// When removing the PIN code. // When removing the PIN code.
keychain.removePINCode() keychain.removePINCode()
// Then the PIN code should no longer be stored. // Then the PIN code should no longer be stored.
try XCTAssertFalse(keychain.containsPINCode(), "The keychain should no longer contain the PIN code.") #expect(try !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(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. // Given a keychain without any biometric state.
XCTAssertFalse(keychain.containsPINCodeBiometricState(), "A new keychain shouldn't contain biometric state.") #expect(!keychain.containsPINCodeBiometricState(), "A new keychain shouldn't contain biometric state.")
XCTAssertNil(keychain.pinCodeBiometricState(), "A new keychain shouldn't return biometric state.") #expect(keychain.pinCodeBiometricState() == nil, "A new keychain shouldn't return biometric state.")
// When setting the state. // When setting the state.
let data = Data("Face ID".utf8) let data = Data("Face ID".utf8)
try keychain.setPINCodeBiometricState(data) try keychain.setPINCodeBiometricState(data)
// Then the state should be stored. // Then the state should be stored.
XCTAssertTrue(keychain.containsPINCodeBiometricState(), "The keychain should contain the biometric state.") #expect(keychain.containsPINCodeBiometricState(), "The keychain should contain the biometric state.")
XCTAssertEqual(keychain.pinCodeBiometricState(), data, "The stored biometric state should match what was set.") #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. // Given a keychain that contains PIN code biometric state.
let data = Data("😃".utf8) let data = Data("😃".utf8)
try keychain.setPINCodeBiometricState(data) try keychain.setPINCodeBiometricState(data)
XCTAssertTrue(keychain.containsPINCodeBiometricState(), "The keychain should contain the biometric state.") #expect(keychain.containsPINCodeBiometricState(), "The keychain should contain the biometric state.")
XCTAssertEqual(keychain.pinCodeBiometricState(), data, "The stored biometric state should match what was set.") #expect(keychain.pinCodeBiometricState() == data, "The stored biometric state should match what was set.")
// When setting different state. // When setting different state.
let newData = Data("😎".utf8) let newData = Data("😎".utf8)
try keychain.setPINCodeBiometricState(newData) try keychain.setPINCodeBiometricState(newData)
// Then the state should be updated. // Then the state should be updated.
XCTAssertTrue(keychain.containsPINCodeBiometricState(), "The keychain should still contain biometric state.") #expect(keychain.containsPINCodeBiometricState(), "The keychain should still contain biometric state.")
XCTAssertNotEqual(keychain.pinCodeBiometricState(), data, "The stored biometric state shouldn't match the old value.") #expect(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.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. // Given a keychain that contains PIN code biometric state.
let data = Data("Face ID".utf8) let data = Data("Face ID".utf8)
try keychain.setPINCodeBiometricState(data) try keychain.setPINCodeBiometricState(data)
XCTAssertTrue(keychain.containsPINCodeBiometricState(), "The keychain should contain the biometric state.") #expect(keychain.containsPINCodeBiometricState(), "The keychain should contain the biometric state.")
XCTAssertEqual(keychain.pinCodeBiometricState(), data, "The stored biometric state should match what was set.") #expect(keychain.pinCodeBiometricState() == data, "The stored biometric state should match what was set.")
// When removing the state. // When removing the state.
keychain.removePINCodeBiometricState() keychain.removePINCodeBiometricState()
// Then the state should no longer be stored. // Then the state should no longer be stored.
XCTAssertFalse(keychain.containsPINCodeBiometricState(), "The keychain should no longer contain the biometric state.") #expect(!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.pinCodeBiometricState() == nil, "There shouldn't be any stored biometric state after removing it.")
} }
} }

View File

@@ -7,25 +7,22 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class KnockRequestsListScreenViewModelTests: XCTestCase { @Suite
var viewModel: KnockRequestsListScreenViewModelProtocol! struct KnockRequestsListScreenViewModelTests {
init() {
var context: KnockRequestsListScreenViewModelType.Context {
viewModel.context
}
override func setUpWithError() throws {
AppSettings.resetAllSettings() AppSettings.resetAllSettings()
} }
func testLoadingState() async throws { @Test
func loadingState() async throws {
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loading, joinRule: .knock)) let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loading, joinRule: .knock))
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock, let viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
mediaProvider: MediaProviderMock(), mediaProvider: MediaProviderMock(),
userIndicatorController: UserIndicatorControllerMock()) userIndicatorController: UserIndicatorControllerMock())
let context = viewModel.context
let deferred = deferFulfillment(context.$viewState) { state in let deferred = deferFulfillment(context.$viewState) { state in
!state.shouldDisplayRequests && !state.shouldDisplayRequests &&
@@ -39,11 +36,13 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
} }
func testEmptyState() async throws { @Test
func emptyState() async throws {
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loaded([]), joinRule: .knock)) let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loaded([]), joinRule: .knock))
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock, let viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
mediaProvider: MediaProviderMock(), mediaProvider: MediaProviderMock(),
userIndicatorController: UserIndicatorControllerMock()) userIndicatorController: UserIndicatorControllerMock())
let context = viewModel.context
let deferred = deferFulfillment(context.$viewState) { state in let deferred = deferFulfillment(context.$viewState) { state in
!state.shouldDisplayRequests && !state.shouldDisplayRequests &&
@@ -57,7 +56,8 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
} }
func testLoadedState() async throws { @Test
func loadedState() async throws {
let roomProxyMock = JoinedRoomProxyMock(.init(members: [.mockAdmin], let roomProxyMock = JoinedRoomProxyMock(.init(members: [.mockAdmin],
knockRequestsState: .loaded([KnockRequestProxyMock(.init(eventID: "1", userID: "@alice:matrix.org")), knockRequestsState: .loaded([KnockRequestProxyMock(.init(eventID: "1", userID: "@alice:matrix.org")),
KnockRequestProxyMock(.init(eventID: "2", userID: "@bob: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"))]), KnockRequestProxyMock(.init(eventID: "4", userID: "@dan:matrix.org"))]),
ownUserID: RoomMemberProxyMock.mockAdmin.userID, ownUserID: RoomMemberProxyMock.mockAdmin.userID,
joinRule: .knock)) joinRule: .knock))
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock, let viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
mediaProvider: MediaProviderMock(), mediaProvider: MediaProviderMock(),
userIndicatorController: UserIndicatorControllerMock()) userIndicatorController: UserIndicatorControllerMock())
let context = viewModel.context
var deferred = deferFulfillment(context.$viewState) { state in var deferred = deferFulfillment(context.$viewState) { state in
state.shouldDisplayRequests && state.shouldDisplayRequests &&
@@ -99,10 +100,7 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
context.send(viewAction: .declineRequest(eventID: "2")) context.send(viewAction: .declineRequest(eventID: "2"))
try await deferred.fulfill() try await deferred.fulfill()
guard let declineAlertInfo = context.alertInfo else { let declineAlertInfo = try #require(context.alertInfo)
XCTFail("Can't be nil")
return
}
deferred = deferFulfillment(context.$viewState) { state in deferred = deferFulfillment(context.$viewState) { state in
state.shouldDisplayRequests && state.shouldDisplayRequests &&
state.handledEventIDs == ["1", "2"] && state.handledEventIDs == ["1", "2"] &&
@@ -119,10 +117,7 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
context.send(viewAction: .ban(eventID: "3")) context.send(viewAction: .ban(eventID: "3"))
try await deferred.fulfill() try await deferred.fulfill()
guard let banAlertInfo = context.alertInfo else { let banAlertInfo = try #require(context.alertInfo)
XCTFail("Can't be nil")
return
}
deferred = deferFulfillment(context.$viewState) { state in deferred = deferFulfillment(context.$viewState) { state in
state.shouldDisplayRequests && state.shouldDisplayRequests &&
state.handledEventIDs == ["1", "2", "3"] && state.handledEventIDs == ["1", "2", "3"] &&
@@ -134,15 +129,17 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
try await deferred.fulfill() 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")), 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: "2", userID: "@bob:matrix.org")),
KnockRequestProxyMock(.init(eventID: "3", userID: "@charlie:matrix.org")), KnockRequestProxyMock(.init(eventID: "3", userID: "@charlie:matrix.org")),
KnockRequestProxyMock(.init(eventID: "4", userID: "@dan:matrix.org"))]), KnockRequestProxyMock(.init(eventID: "4", userID: "@dan:matrix.org"))]),
joinRule: .knock)) joinRule: .knock))
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock, let viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
mediaProvider: MediaProviderMock(), mediaProvider: MediaProviderMock(),
userIndicatorController: UserIndicatorControllerMock()) userIndicatorController: UserIndicatorControllerMock())
let context = viewModel.context
var deferred = deferFulfillment(context.$viewState) { state in var deferred = deferFulfillment(context.$viewState) { state in
state.shouldDisplayRequests && state.shouldDisplayRequests &&
@@ -164,10 +161,7 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
context.send(viewAction: .acceptAllRequests) context.send(viewAction: .acceptAllRequests)
try await deferred.fulfill() try await deferred.fulfill()
guard let alertInfo = context.alertInfo else { let alertInfo = try #require(context.alertInfo)
XCTFail("Can't be nil")
return
}
deferred = deferFulfillment(context.$viewState) { state in deferred = deferFulfillment(context.$viewState) { state in
!state.shouldDisplayRequests && !state.shouldDisplayRequests &&
@@ -179,7 +173,8 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
try await deferred.fulfill() 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 // 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], let roomProxyMock = JoinedRoomProxyMock(.init(members: [.mockAdmin],
knockRequestsState: .loaded([KnockRequestProxyMock(.init(eventID: "1", userID: "@alice:matrix.org")), 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"))]), KnockRequestProxyMock(.init(eventID: "4", userID: "@dan:matrix.org"))]),
ownUserID: RoomMemberProxyMock.mockAdmin.userID, ownUserID: RoomMemberProxyMock.mockAdmin.userID,
joinRule: .invite)) joinRule: .invite))
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock, let viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
mediaProvider: MediaProviderMock(), mediaProvider: MediaProviderMock(),
userIndicatorController: UserIndicatorControllerMock()) userIndicatorController: UserIndicatorControllerMock())
let context = viewModel.context
let deferred = deferFulfillment(context.$viewState) { state in let deferred = deferFulfillment(context.$viewState) { state in
!state.shouldDisplayRequests && !state.shouldDisplayRequests &&
@@ -201,7 +197,8 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
try await deferred.fulfill() 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 // 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")), 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: "2", userID: "@bob:matrix.org")),
@@ -209,9 +206,10 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
KnockRequestProxyMock(.init(eventID: "4", userID: "@dan:matrix.org"))]), KnockRequestProxyMock(.init(eventID: "4", userID: "@dan:matrix.org"))]),
joinRule: .knock, joinRule: .knock,
powerLevelsConfiguration: .init(canUserInvite: false))) powerLevelsConfiguration: .init(canUserInvite: false)))
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock, let viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
mediaProvider: MediaProviderMock(), mediaProvider: MediaProviderMock(),
userIndicatorController: UserIndicatorControllerMock()) userIndicatorController: UserIndicatorControllerMock())
let context = viewModel.context
let deferred = deferFulfillment(context.$viewState) { state in let deferred = deferFulfillment(context.$viewState) { state in
!state.shouldDisplayRequests && !state.shouldDisplayRequests &&

View File

@@ -1,13 +0,0 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
@testable import ElementX
import XCTest
@MainActor
class LegalInformationScreenViewModelTests: XCTestCase { }

View File

@@ -7,72 +7,79 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Foundation
import Testing
class LocalizationTests: XCTestCase { @Suite
override func tearDown() { final class LocalizationTests {
super.tearDown() deinit {
Bundle.overrideLocalizations = nil Bundle.overrideLocalizations = nil
} }
/// Test ElementL10n considers app language changes /// Test ElementL10n considers app language changes
func testAppLanguage() { @Test
func appLanguage() {
// set app language to English // set app language to English
Bundle.overrideLocalizations = ["en"] Bundle.overrideLocalizations = ["en"]
XCTAssertEqual(L10n.testLanguageIdentifier, "en") #expect(L10n.testLanguageIdentifier == "en")
// set app language to Italian // set app language to Italian
Bundle.overrideLocalizations = ["it"] Bundle.overrideLocalizations = ["it"]
XCTAssertEqual(L10n.testLanguageIdentifier, "it") #expect(L10n.testLanguageIdentifier == "it")
} }
/// Test fallback language for a language not supported at all /// 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) // set app language to something Element don't support at all (chose non existing identifier)
Bundle.overrideLocalizations = ["xx"] Bundle.overrideLocalizations = ["xx"]
XCTAssertEqual(L10n.testLanguageIdentifier, "en") #expect(L10n.testLanguageIdentifier == "en")
} }
/// Test fallback language for a language supported but poorly translated /// 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) // 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"] Bundle.overrideLocalizations = ["it"]
XCTAssertEqual(L10n.testLanguageIdentifier, "it") #expect(L10n.testLanguageIdentifier == "it")
XCTAssertEqual(L10n.testUntranslatedDefaultLanguageIdentifier, "en") #expect(L10n.testUntranslatedDefaultLanguageIdentifier == "en")
} }
/// Test plurals that ElementL10n considers app language changes /// Test plurals that ElementL10n considers app language changes
func testPlurals() { @Test
func plurals() {
// set app language to English // set app language to English
Bundle.overrideLocalizations = ["en"] Bundle.overrideLocalizations = ["en"]
XCTAssertEqual(L10n.commonMemberCount(1), "1 Member") #expect(L10n.commonMemberCount(1) == "1 Member")
XCTAssertEqual(L10n.commonMemberCount(2), "2 Members") #expect(L10n.commonMemberCount(2) == "2 Members")
// set app language to Italian // set app language to Italian
Bundle.overrideLocalizations = ["it"] Bundle.overrideLocalizations = ["it"]
XCTAssertEqual(L10n.commonMemberCount(1), "1 Membro") #expect(L10n.commonMemberCount(1) == "1 Membro")
XCTAssertEqual(L10n.commonMemberCount(2), "2 Membri") #expect(L10n.commonMemberCount(2) == "2 Membri")
} }
/// Test plurals fallback language for a language not supported at all /// 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") // set app language to something Element don't support at all ("invalid identifier")
Bundle.overrideLocalizations = ["xx"] Bundle.overrideLocalizations = ["xx"]
XCTAssertEqual(L10n.commonMemberCount(1), "1 Member") #expect(L10n.commonMemberCount(1) == "1 Member")
XCTAssertEqual(L10n.commonMemberCount(2), "2 Members") #expect(L10n.commonMemberCount(2) == "2 Members")
} }
/// Test untranslated strings /// Test untranslated strings
func testUntranslated() { @Test
XCTAssertEqual(UntranslatedL10n.untranslated, "Untranslated") func untranslated() {
XCTAssertEqual(UntranslatedL10n.untranslatedPlural(1), "One untranslated item") #expect(UntranslatedL10n.untranslated == "Untranslated")
XCTAssertEqual(UntranslatedL10n.untranslatedPlural(5), "5 untranslated items") #expect(UntranslatedL10n.untranslatedPlural(1) == "One untranslated item")
#expect(UntranslatedL10n.untranslatedPlural(5) == "5 untranslated items")
} }
} }

View File

@@ -7,65 +7,66 @@
// //
@testable import ElementX @testable import ElementX
import Foundation
@testable import MatrixRustSDK @testable import MatrixRustSDK
import XCTest import Testing
class LoggingTests: XCTestCase { @Suite
final class LoggingTests {
private enum Constants { private enum Constants {
static let genericFailure = "Test failed" static let genericFailure = "Test failed"
} }
override func tearDown() async throws { deinit {
Tracing.logsDirectoryOverride = nil Tracing.logsDirectoryOverride = nil
try reloadTracingFileWriter(configuration: .init(path: URL.appGroupLogsDirectory.path(percentEncoded: false), do {
filePrefix: "console-tests", try reloadTracingFileWriter(configuration: .init(path: URL.appGroupLogsDirectory.path(percentEncoded: false),
fileSuffix: ".log", filePrefix: "console-tests",
maxTotalSizeBytes: 1000, fileSuffix: ".log",
maxAgeSeconds: 1000)) maxTotalSizeBytes: 1000,
maxAgeSeconds: 1000))
} catch {
Issue.record(error)
}
} }
func testFileLogging() throws { @Test
func fileLogging() throws {
try setupTest() try setupTest()
let infoLog = UUID().uuidString let infoLog = UUID().uuidString
MXLog.info(infoLog) MXLog.info(infoLog)
guard let logFile = Tracing.logFiles.first else { let logFile = try #require(Tracing.logFiles.first)
XCTFail(Constants.genericFailure)
return
}
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() try setupTest()
let verboseLog = UUID().uuidString let verboseLog = UUID().uuidString
MXLog.verbose(verboseLog) MXLog.verbose(verboseLog)
guard let logFile = Tracing.logFiles.first else { let logFile = try #require(Tracing.logFiles.first)
XCTFail(Constants.genericFailure)
return
}
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 /// 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 😕. /// 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) MXLog.info(UUID().uuidString)
guard let logFile = Tracing.logFiles.first else { let logFile = try #require(Tracing.logFiles.first)
XCTFail(Constants.genericFailure)
return
}
let target = "tests" let target = "tests"
XCTAssertTrue(logFile.lastPathComponent.contains(target)) #expect(logFile.lastPathComponent.contains(target))
} }
func testRoomSummaryContentIsRedacted() throws { @Test
func roomSummaryContentIsRedacted() throws {
try setupTest() try setupTest()
// Given a room summary that contains sensitive information // Given a room summary that contains sensitive information
@@ -99,19 +100,17 @@ class LoggingTests: XCTestCase {
MXLog.info(roomSummary) MXLog.info(roomSummary)
// Then the log file should not include the sensitive information // Then the log file should not include the sensitive information
guard let logFile = Tracing.logFiles.first else { let logFile = try #require(Tracing.logFiles.first)
XCTFail(Constants.genericFailure)
return
}
let content = try String(contentsOf: logFile, encoding: .utf8) let content = try String(contentsOf: logFile, encoding: .utf8)
XCTAssertTrue(content.contains(roomSummary.id)) #expect(content.contains(roomSummary.id))
XCTAssertFalse(content.contains(roomName)) #expect(!content.contains(roomName))
XCTAssertFalse(content.contains(lastMessage)) #expect(!content.contains(lastMessage))
XCTAssertFalse(content.contains(heroName)) #expect(!content.contains(heroName))
} }
func testTimelineContentIsRedacted() throws { @Test
func timelineContentIsRedacted() throws {
try setupTest() try setupTest()
// Given timeline items that contain text // Given timeline items that contain text
@@ -181,35 +180,33 @@ class LoggingTests: XCTestCase {
MXLog.info(fileMessage) MXLog.info(fileMessage)
// Then the log file should not include the text content // Then the log file should not include the text content
guard let logFile = Tracing.logFiles.first else { let logFile = try #require(Tracing.logFiles.first)
XCTFail(Constants.genericFailure)
return
}
let content = try String(contentsOf: logFile, encoding: .utf8) let content = try String(contentsOf: logFile, encoding: .utf8)
XCTAssertTrue(content.contains(textMessage.id.uniqueID.value)) #expect(content.contains(textMessage.id.uniqueID.value))
XCTAssertFalse(content.contains(textMessage.body)) #expect(!content.contains(textMessage.body))
XCTAssertFalse(content.contains(textAttributedString)) #expect(!content.contains(textAttributedString))
XCTAssertTrue(content.contains(noticeMessage.id.uniqueID.value)) #expect(content.contains(noticeMessage.id.uniqueID.value))
XCTAssertFalse(content.contains(noticeMessage.body)) #expect(!content.contains(noticeMessage.body))
XCTAssertFalse(content.contains(noticeAttributedString)) #expect(!content.contains(noticeAttributedString))
XCTAssertTrue(content.contains(emoteMessage.id.uniqueID.value)) #expect(content.contains(emoteMessage.id.uniqueID.value))
XCTAssertFalse(content.contains(emoteMessage.body)) #expect(!content.contains(emoteMessage.body))
XCTAssertFalse(content.contains(emoteAttributedString)) #expect(!content.contains(emoteAttributedString))
XCTAssertTrue(content.contains(imageMessage.id.uniqueID.value)) #expect(content.contains(imageMessage.id.uniqueID.value))
XCTAssertFalse(content.contains(imageMessage.body)) #expect(!content.contains(imageMessage.body))
XCTAssertTrue(content.contains(videoMessage.id.uniqueID.value)) #expect(content.contains(videoMessage.id.uniqueID.value))
XCTAssertFalse(content.contains(videoMessage.body)) #expect(!content.contains(videoMessage.body))
XCTAssertTrue(content.contains(fileMessage.id.uniqueID.value)) #expect(content.contains(fileMessage.id.uniqueID.value))
XCTAssertFalse(content.contains(fileMessage.body)) #expect(!content.contains(fileMessage.body))
} }
func testRustMessageContentIsRedacted() throws { @Test
func rustMessageContentIsRedacted() throws {
try setupTest() try setupTest()
// Given message content that contain text // Given message content that contain text
@@ -250,36 +247,34 @@ class LoggingTests: XCTestCase {
MXLog.info(rustFileMessage) MXLog.info(rustFileMessage)
// Then the log file should not include the text content // Then the log file should not include the text content
guard let logFile = Tracing.logFiles.first else { let logFile = try #require(Tracing.logFiles.first)
XCTFail(Constants.genericFailure)
return
}
let content = try String(contentsOf: logFile, encoding: .utf8) let content = try String(contentsOf: logFile, encoding: .utf8)
XCTAssertTrue(content.contains(String(describing: TextMessageContent.self))) #expect(content.contains(String(describing: TextMessageContent.self)))
XCTAssertFalse(content.contains(textString)) #expect(!content.contains(textString))
XCTAssertTrue(content.contains(String(describing: NoticeMessageContent.self))) #expect(content.contains(String(describing: NoticeMessageContent.self)))
XCTAssertFalse(content.contains(noticeString)) #expect(!content.contains(noticeString))
XCTAssertTrue(content.contains(String(describing: EmoteMessageContent.self))) #expect(content.contains(String(describing: EmoteMessageContent.self)))
XCTAssertFalse(content.contains(emoteString)) #expect(!content.contains(emoteString))
XCTAssertTrue(content.contains(String(describing: ImageMessageContent.self))) #expect(content.contains(String(describing: ImageMessageContent.self)))
XCTAssertFalse(content.contains(rustImageMessage.filename)) #expect(!content.contains(rustImageMessage.filename))
XCTAssertTrue(content.contains(String(describing: VideoMessageContent.self))) #expect(content.contains(String(describing: VideoMessageContent.self)))
XCTAssertFalse(content.contains(rustVideoMessage.filename)) #expect(!content.contains(rustVideoMessage.filename))
XCTAssertTrue(content.contains(String(describing: FileMessageContent.self))) #expect(content.contains(String(describing: FileMessageContent.self)))
XCTAssertFalse(content.contains(rustFileMessage.filename)) #expect(!content.contains(rustFileMessage.filename))
} }
func testLogFileSorting() throws { @Test
func logFileSorting() throws {
try setupTest(redirectTracingFileWriter: false) try setupTest(redirectTracingFileWriter: false)
// Given a collection of log files. // Given a collection of log files.
XCTAssertTrue(Tracing.logFiles.isEmpty) #expect(Tracing.logFiles.isEmpty)
// When creating new logs. // When creating new logs.
let logsFileDirectory = Tracing.logsDirectory let logsFileDirectory = Tracing.logsDirectory
@@ -294,17 +289,17 @@ class LoggingTests: XCTestCase {
} }
// Then the logs should be sorted chronologically (newest first) and not alphabetically. // Then the logs should be sorted chronologically (newest first) and not alphabetically.
XCTAssertEqual(Tracing.logFiles.map(\.lastPathComponent), #expect(Tracing.logFiles.map(\.lastPathComponent) ==
["console-nse.5.log", ["console-nse.5.log",
"console-nse.4.log", "console-nse.4.log",
"console-nse.3.log", "console-nse.3.log",
"console-nse.2.log", "console-nse.2.log",
"console-nse.1.log", "console-nse.1.log",
"console.5.log", "console.5.log",
"console.4.log", "console.4.log",
"console.3.log", "console.3.log",
"console.2.log", "console.2.log",
"console.1.log"]) "console.1.log"])
// When updating the oldest log file. // When updating the oldest log file.
let currentLogFile = logsFileDirectory.appending(path: "console.1.log") let currentLogFile = logsFileDirectory.appending(path: "console.1.log")
@@ -314,17 +309,17 @@ class LoggingTests: XCTestCase {
try fileHandle.close() try fileHandle.close()
// Then that file should now be the first log file. // Then that file should now be the first log file.
XCTAssertEqual(Tracing.logFiles.map(\.lastPathComponent), #expect(Tracing.logFiles.map(\.lastPathComponent) ==
["console.1.log", ["console.1.log",
"console-nse.5.log", "console-nse.5.log",
"console-nse.4.log", "console-nse.4.log",
"console-nse.3.log", "console-nse.3.log",
"console-nse.2.log", "console-nse.2.log",
"console-nse.1.log", "console-nse.1.log",
"console.5.log", "console.5.log",
"console.4.log", "console.4.log",
"console.3.log", "console.3.log",
"console.2.log"]) "console.2.log"])
} }
// MARK: - Helpers // 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 // 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. // in the directory, ready to be written to.
XCTAssertTrue(Tracing.logFiles.isEmpty) #expect(Tracing.logFiles.isEmpty)
if redirectTracingFileWriter { if redirectTracingFileWriter {
try reloadTracingFileWriter(configuration: .init(path: testDirectory.path(percentEncoded: false), try reloadTracingFileWriter(configuration: .init(path: testDirectory.path(percentEncoded: false),

View File

@@ -7,10 +7,11 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class LoginScreenViewModelTests: XCTestCase { @Suite
struct LoginScreenViewModelTests {
var viewModel: LoginScreenViewModelProtocol! var viewModel: LoginScreenViewModelProtocol!
var context: LoginScreenViewModelType.Context { var context: LoginScreenViewModelType.Context {
viewModel.context viewModel.context
@@ -19,161 +20,214 @@ class LoginScreenViewModelTests: XCTestCase {
var clientFactory: AuthenticationClientFactoryMock! var clientFactory: AuthenticationClientFactoryMock!
var service: AuthenticationServiceProtocol! 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. // Given the view model configured for a basic server example.com that only supports password authentication.
await setupViewModel() await setupViewModel()
// Then the view state should be updated with the homeserver and show the login form. // 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.") #expect(context.viewState.homeserver == .mockBasicServer,
XCTAssertEqual(context.viewState.loginMode, .password, "The login form should be shown.") "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. // Given a form with an empty username and password.
await setupViewModel() await setupViewModel()
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.") #expect(context.password.isEmpty,
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.") "The initial value for the password should be empty.")
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") #expect(context.username.isEmpty,
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.") "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. // When entering a username without a password.
context.username = "bob" context.username = "bob"
context.password = "" context.password = ""
// Then the credentials should be considered invalid. // Then the credentials should be considered invalid.
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") #expect(!context.viewState.hasValidCredentials,
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.") "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. // Given a form with an empty username and password.
await setupViewModel() await setupViewModel()
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.") #expect(context.password.isEmpty,
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.") "The initial value for the password should be empty.")
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") #expect(context.username.isEmpty,
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.") "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. // When entering a password without a username.
context.username = "" context.username = ""
context.password = "12345678" context.password = "12345678"
// Then the credentials should be considered invalid. // Then the credentials should be considered invalid.
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") #expect(!context.viewState.hasValidCredentials,
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.") "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. // Given a form with an empty username and password.
await setupViewModel() await setupViewModel()
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.") #expect(context.password.isEmpty,
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.") "The initial value for the password should be empty.")
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") #expect(context.username.isEmpty,
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.") "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. // When entering a username and an 8-character password.
context.username = "bob" context.username = "bob"
context.password = "12345678" context.password = "12345678"
// Then the credentials should be considered valid. // Then the credentials should be considered valid.
XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid when the username and password are valid.") #expect(context.viewState.hasValidCredentials,
XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.") "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. // Given a form with valid credentials.
await setupViewModel() await setupViewModel()
context.username = "@bob:example.com" context.username = "@bob:example.com"
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be not be valid without a password.") #expect(!context.viewState.hasValidCredentials,
XCTAssertFalse(context.viewState.isLoading, "The view shouldn't start in a loading state.") "The credentials should be not be valid without a password.")
XCTAssertFalse(context.viewState.canSubmit, "The form should not be submittable.") #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. // 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) context.send(viewAction: .parseUsername)
// Then the view state should represent the loading but never allow submitting to occur. // Then the view state should represent the loading but never allow submitting to occur.
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertFalse(context.viewState.isLoading, "The view should be back in a loaded state.") #expect(!context.viewState.isLoading,
XCTAssertFalse(context.viewState.canSubmit, "The form should still not be submittable.") "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. // Given a form with valid credentials.
await setupViewModel() await setupViewModel()
context.username = "@bob:example.com" context.username = "@bob:example.com"
context.password = "12345678" context.password = "12345678"
XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid.") #expect(context.viewState.hasValidCredentials,
XCTAssertFalse(context.viewState.isLoading, "The view shouldn't start in a loading state.") "The credentials should be valid.")
XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.") #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. // 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) context.send(viewAction: .parseUsername)
// Then the view should be blocked from submitting while loading and then become unblocked again. // Then the view should be blocked from submitting while loading and then become unblocked again.
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertFalse(context.viewState.isLoading, "The view should be back in a loaded state.") #expect(!context.viewState.isLoading,
XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.") "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 // Given the screen configured for matrix.org
await setupViewModel() await setupViewModel()
// When entering a username for a user on a homeserver with OIDC. // 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.username = "@bob:company.com"
context.send(viewAction: .parseUsername) context.send(viewAction: .parseUsername)
try await deferred.fulfill() try await deferred.fulfill()
// Then the view state should be updated with the homeserver and show the OIDC button. // 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 // Given the screen configured for matrix.org
await setupViewModel() 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. // 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.username = "@bob:server.net"
context.send(viewAction: .parseUsername) context.send(viewAction: .parseUsername)
try await deferred.fulfill() try await deferred.fulfill()
// Then the view state should be updated to show an alert. // 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 // Given the screen configured for matrix.org
await setupViewModel() 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. // 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.username = "@bob:secure.gov"
context.send(viewAction: .parseUsername) context.send(viewAction: .parseUsername)
try await deferred.fulfill() try await deferred.fulfill()
// Then the view state should be updated to show an alert. // 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: "") await setupViewModel(loginHint: "")
XCTAssertEqual(context.username, "") #expect(context.username == "")
await setupViewModel(loginHint: "alice") await setupViewModel(loginHint: "alice")
XCTAssertEqual(context.username, "alice") #expect(context.username == "alice")
await setupViewModel(loginHint: "mxid:@alice:example.com") await setupViewModel(loginHint: "mxid:@alice:example.com")
XCTAssertEqual(context.username, "@alice:example.com") #expect(context.username == "@alice:example.com")
} }
// MARK: - Helpers // 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()) clientFactory = AuthenticationClientFactoryMock(configuration: .init())
service = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()), service = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
encryptionKeyProvider: EncryptionKeyProvider(), encryptionKeyProvider: EncryptionKeyProvider(),
@@ -181,8 +235,9 @@ class LoginScreenViewModelTests: XCTestCase {
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
appHooks: AppHooks()) appHooks: AppHooks())
guard case .success = await service.configure(for: homeserverAddress, flow: .login) else { guard case .success = await service
XCTFail("A valid server should be configured for the test.") .configure(for: homeserverAddress, flow: .login) else {
Issue.record("A valid server should be configured for the test.")
return return
} }

View File

@@ -7,23 +7,25 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class ManageRoomMemberSheetViewModelTests: XCTestCase { @Suite
struct ManageRoomMemberSheetViewModelTests {
private var viewModel: ManageRoomMemberSheetViewModel! private var viewModel: ManageRoomMemberSheetViewModel!
private var context: ManageRoomMemberSheetViewModel.Context! { private var context: ManageRoomMemberSheetViewModel.Context! {
viewModel.context viewModel.context
} }
func testKick() async throws { @Test
mutating func kick() async throws {
let testReason = "Kick Test" let testReason = "Kick Test"
let roomProxy = JoinedRoomProxyMock(.init(members: [RoomMemberProxyMock.mockAdmin, RoomMemberProxyMock.mockAlice])) let roomProxy = JoinedRoomProxyMock(.init(members: [RoomMemberProxyMock.mockAdmin, RoomMemberProxyMock.mockAlice]))
let expectation = XCTestExpectation(description: "Kick member") var kickCalled = false
roomProxy.kickUserReasonClosure = { userID, reason in roomProxy.kickUserReasonClosure = { userID, reason in
defer { expectation.fulfill() } kickCalled = true
XCTAssertEqual(userID, RoomMemberProxyMock.mockAlice.userID) #expect(userID == RoomMemberProxyMock.mockAlice.userID)
XCTAssertEqual(reason, testReason) #expect(reason == testReason)
return .success(()) return .success(())
} }
@@ -43,18 +45,19 @@ class ManageRoomMemberSheetViewModelTests: XCTestCase {
context.alertInfo?.textFields?[0].text.wrappedValue = testReason context.alertInfo?.textFields?[0].text.wrappedValue = testReason
context.alertInfo?.secondaryButton?.action?() context.alertInfo?.secondaryButton?.action?()
await fulfillment(of: [expectation])
try await deferredAction.fulfill() try await deferredAction.fulfill()
#expect(kickCalled)
} }
func testBan() async throws { @Test
mutating func ban() async throws {
let testReason = "Ban Test" let testReason = "Ban Test"
let roomProxy = JoinedRoomProxyMock(.init(members: [RoomMemberProxyMock.mockAdmin, RoomMemberProxyMock.mockAlice])) let roomProxy = JoinedRoomProxyMock(.init(members: [RoomMemberProxyMock.mockAdmin, RoomMemberProxyMock.mockAlice]))
let expectation = XCTestExpectation(description: "Ban member") var banCalled = false
roomProxy.banUserReasonClosure = { userID, reason in roomProxy.banUserReasonClosure = { userID, reason in
defer { expectation.fulfill() } banCalled = true
XCTAssertEqual(userID, RoomMemberProxyMock.mockAlice.userID) #expect(userID == RoomMemberProxyMock.mockAlice.userID)
XCTAssertEqual(reason, testReason) #expect(reason == testReason)
return .success(()) return .success(())
} }
@@ -74,11 +77,12 @@ class ManageRoomMemberSheetViewModelTests: XCTestCase {
} }
context.alertInfo?.textFields?[0].text.wrappedValue = testReason context.alertInfo?.textFields?[0].text.wrappedValue = testReason
context.alertInfo?.secondaryButton?.action?() context.alertInfo?.secondaryButton?.action?()
await fulfillment(of: [expectation])
try await deferredAction.fulfill() 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])) let roomProxy = JoinedRoomProxyMock(.init(members: [RoomMemberProxyMock.mockAdmin, RoomMemberProxyMock.mockAlice]))
viewModel = ManageRoomMemberSheetViewModel(memberDetails: .memberDetails(roomMember: .init(withProxy: RoomMemberProxyMock.mockAlice)), viewModel = ManageRoomMemberSheetViewModel(memberDetails: .memberDetails(roomMember: .init(withProxy: RoomMemberProxyMock.mockAlice)),
permissions: .init(canKick: true, canBan: true, ownPowerLevel: RoomMemberProxyMock.mockAdmin.powerLevel), permissions: .init(canKick: true, canBan: true, ownPowerLevel: RoomMemberProxyMock.mockAdmin.powerLevel),
@@ -92,6 +96,6 @@ class ManageRoomMemberSheetViewModelTests: XCTestCase {
} }
context.send(viewAction: .displayDetails) context.send(viewAction: .displayDetails)
try await deferredAction.fulfill() try await deferredAction.fulfill()
XCTAssertNil(context.alertInfo) #expect(context.alertInfo == nil)
} }
} }

View File

@@ -8,24 +8,26 @@
import CoreLocation import CoreLocation
@testable import ElementX @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 baseURL: URL = "http://www.foo.com"
private static let apiKey = "some_key" private static let apiKey = "some_key"
private static let lightStyleID = "9bc819c8-e627-474a-a348-ec144fe3d810" private static let lightStyleID = "9bc819c8-e627-474a-a348-ec144fe3d810"
private static let darkStyleID = "dea61faf-292b-4774-9660-58fcef89a7f3" private static let darkStyleID = "dea61faf-292b-4774-9660-58fcef89a7f3"
var builder: MapTilerURLBuilderProtocol! var builder: MapTilerURLBuilderProtocol
override func setUp() { init() {
builder = MapTilerConfiguration(baseURL: Self.baseURL, builder = MapTilerConfiguration(baseURL: Self.baseURL,
apiKey: Self.apiKey, apiKey: Self.apiKey,
lightStyleID: Self.lightStyleID, lightStyleID: Self.lightStyleID,
darkStyleID: Self.darkStyleID) darkStyleID: Self.darkStyleID)
} }
func testStaticMapBuilder() { @Test
func staticMapBuilder() {
let url = builder.staticMapTileImageURL(for: .light, let url = builder.staticMapTileImageURL(for: .light,
coordinates: .init(latitude: 1, longitude: 2), coordinates: .init(latitude: 1, longitude: 2),
zoomLevel: 5, zoomLevel: 5,
@@ -33,10 +35,11 @@ final class MapTilerURLBuilderTests: XCTestCase {
attribution: .hidden) 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" 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, let url = builder.staticMapTileImageURL(for: .dark,
coordinates: .init(latitude: 1, longitude: 2), coordinates: .init(latitude: 1, longitude: 2),
zoomLevel: 5, zoomLevel: 5,
@@ -44,21 +47,23 @@ final class MapTilerURLBuilderTests: XCTestCase {
attribution: .topLeft) 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" 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) #expect(url == expectedURL)
} }
func testDynamicMapBuilder() { @Test
func dynamicMapBuilder() {
let url = builder.interactiveMapURL(for: .dark) let url = builder.interactiveMapURL(for: .dark)
let expectedURL: URL = "http://www.foo.com/dea61faf-292b-4774-9660-58fcef89a7f3/style.json?key=some_key" 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
mutating func nilAPIKey() {
let configuration = MapTilerConfiguration(baseURL: Self.baseURL, let configuration = MapTilerConfiguration(baseURL: Self.baseURL,
apiKey: nil, apiKey: nil,
lightStyleID: Self.lightStyleID, lightStyleID: Self.lightStyleID,
darkStyleID: Self.darkStyleID) darkStyleID: Self.darkStyleID)
XCTAssertFalse(configuration.isEnabled) #expect(!configuration.isEnabled)
builder = configuration builder = configuration
@@ -67,9 +72,9 @@ final class MapTilerURLBuilderTests: XCTestCase {
zoomLevel: 5, zoomLevel: 5,
size: .init(width: 300, height: 200), size: .init(width: 300, height: 200),
attribution: .topLeft) attribution: .topLeft)
XCTAssertNil(staticMapURL) #expect(staticMapURL == nil)
let dynamicMapURL = builder.interactiveMapURL(for: .light) let dynamicMapURL = builder.interactiveMapURL(for: .light)
XCTAssertNil(dynamicMapURL) #expect(dynamicMapURL == nil)
} }
} }

View File

@@ -8,65 +8,71 @@
@testable import ElementX @testable import ElementX
import Foundation import Foundation
import XCTest import Testing
class MatrixEntityRegexTests: XCTestCase { @Suite
func testHomeserver() { struct MatrixEntityRegexTests {
XCTAssertTrue(MatrixEntityRegex.isMatrixHomeserver("matrix.org")) @Test
XCTAssertTrue(MatrixEntityRegex.isMatrixHomeserver("MATRIX.ORG")) func homeserver() {
XCTAssertFalse(MatrixEntityRegex.isMatrixHomeserver("matrix?.org")) #expect(MatrixEntityRegex.isMatrixHomeserver("matrix.org"))
#expect(MatrixEntityRegex.isMatrixHomeserver("MATRIX.ORG"))
#expect(!MatrixEntityRegex.isMatrixHomeserver("matrix?.org"))
} }
func testUserID() { @Test
XCTAssertTrue(MatrixEntityRegex.isMatrixUserIdentifier("@username:example.com")) func userID() {
XCTAssertFalse(MatrixEntityRegex.isMatrixUserIdentifier("username:example.com")) #expect(MatrixEntityRegex.isMatrixUserIdentifier("@username:example.com"))
XCTAssertFalse(MatrixEntityRegex.isMatrixUserIdentifier("@username.example.com")) #expect(!MatrixEntityRegex.isMatrixUserIdentifier("username:example.com"))
#expect(!MatrixEntityRegex.isMatrixUserIdentifier("@username.example.com"))
} }
func testRoomAlias() { @Test
XCTAssertTrue(MatrixEntityRegex.isMatrixRoomAlias("#element-ios:matrix.org")) func roomAlias() {
XCTAssertFalse(MatrixEntityRegex.isMatrixRoomAlias("element-ios:matrix.org")) #expect(MatrixEntityRegex.isMatrixRoomAlias("#element-ios:matrix.org"))
XCTAssertFalse(MatrixEntityRegex.isMatrixRoomAlias("#element-ios.matrix.org")) #expect(!MatrixEntityRegex.isMatrixRoomAlias("element-ios:matrix.org"))
#expect(!MatrixEntityRegex.isMatrixRoomAlias("#element-ios.matrix.org"))
} }
func testMatrixURI() { @Test
func matrixURI() {
// Users // Users
XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:u/alice:example.org")) #expect(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?action=chat"))
// Room ID // Room ID
XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:roomid/somewhere:example.org")) #expect(MatrixEntityRegex.isMatrixURI("matrix:roomid/somewhere:example.org"))
XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:roomid/my-room:example.com?via=elsewhere.ca")) #expect(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/123_room:chat.myserver.net?via=elsewhere.ca&via=other.org"))
// Room Alias // Room Alias
XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:r/general:matrix.org")) #expect(MatrixEntityRegex.isMatrixURI("matrix:r/general:matrix.org"))
XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:r/123_room:chat.myserver.net")) #expect(MatrixEntityRegex.isMatrixURI("matrix:r/123_room:chat.myserver.net"))
// Event // Event
XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:roomid/somewhere:example.org/e/event")) #expect(MatrixEntityRegex.isMatrixURI("matrix:roomid/somewhere:example.org/e/event"))
XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:roomid/my-room:example.com/e/message?via=elsewhere.ca")) #expect(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/123_room:chat.myserver.net/e/1234?via=elsewhere.ca&via=other.org"))
// Inline // Inline
let string = "Hello matrix:u/alice:example.org how are you?" let string = "Hello matrix:u/alice:example.org how are you?"
XCTAssertFalse(MatrixEntityRegex.isMatrixURI("Hello matrix:u/alice:example.org how are you?")) #expect(!MatrixEntityRegex.isMatrixURI("Hello matrix:u/alice:example.org how are you?"))
XCTAssertEqual(MatrixEntityRegex.uriRegex.matches(in: string).count, 1) #expect(MatrixEntityRegex.uriRegex.matches(in: string).count == 1)
// Invalid // Invalid
XCTAssertFalse(MatrixEntityRegex.isMatrixURI("matrix://@alice:example.org")) #expect(!MatrixEntityRegex.isMatrixURI("matrix://@alice:example.org"))
XCTAssertFalse(MatrixEntityRegex.isMatrixURI("matrix://!somewhere:example.org")) #expect(!MatrixEntityRegex.isMatrixURI("matrix://!somewhere:example.org"))
XCTAssertFalse(MatrixEntityRegex.isMatrixURI("matrix://#general:matrix.org")) #expect(!MatrixEntityRegex.isMatrixURI("matrix://#general:matrix.org"))
XCTAssertFalse(MatrixEntityRegex.isMatrixURI("matrix:event/somewhere:example.org/e/event")) #expect(!MatrixEntityRegex.isMatrixURI("matrix:event/somewhere:example.org/e/event"))
XCTAssertFalse(MatrixEntityRegex.isMatrixURI("matrix:e/somewhere:example.org/e/event")) #expect(!MatrixEntityRegex.isMatrixURI("matrix:e/somewhere:example.org/e/event"))
} }
func testAllUsers() { @Test
XCTAssertTrue(MatrixEntityRegex.containsMatrixAllUsers("@room")) func allUsers() {
XCTAssertTrue(MatrixEntityRegex.containsMatrixAllUsers("a@rooma")) #expect(MatrixEntityRegex.containsMatrixAllUsers("@room"))
XCTAssertTrue(MatrixEntityRegex.containsMatrixAllUsers("a @room a")) #expect(MatrixEntityRegex.containsMatrixAllUsers("a@rooma"))
XCTAssertFalse(MatrixEntityRegex.containsMatrixAllUsers("a @roaom a")) #expect(MatrixEntityRegex.containsMatrixAllUsers("a @room a"))
XCTAssertFalse(MatrixEntityRegex.containsMatrixAllUsers("@roaom")) #expect(!MatrixEntityRegex.containsMatrixAllUsers("a @roaom a"))
XCTAssertTrue(MatrixEntityRegex.containsMatrixAllUsers("@room\n")) #expect(!MatrixEntityRegex.containsMatrixAllUsers("@roaom"))
#expect(MatrixEntityRegex.containsMatrixAllUsers("@room\n"))
} }
} }

View File

@@ -9,34 +9,37 @@
import Combine import Combine
@testable import ElementX @testable import ElementX
import Foundation import Foundation
import XCTest import Testing
@MainActor @MainActor
class MediaPlayerProviderTests: XCTestCase { @Suite
private var mediaPlayerProvider: MediaPlayerProvider! struct MediaPlayerProviderTests {
private var mediaPlayerProvider: MediaPlayerProvider
private let oggMimeType = "audio/ogg" private let oggMimeType = "audio/ogg"
private let someURL = URL.mockMXCAudio private let someURL = URL.mockMXCAudio
private let someOtherURL = URL.mockMXCFile private let someOtherURL = URL.mockMXCFile
override func setUp() async throws { init() async {
mediaPlayerProvider = MediaPlayerProvider() mediaPlayerProvider = MediaPlayerProvider()
} }
func testPlayerStates() { @Test
func playerStates() {
let audioPlayerStateId = AudioPlayerStateIdentifier.timelineItemIdentifier(.randomEvent) let audioPlayerStateId = AudioPlayerStateIdentifier.timelineItemIdentifier(.randomEvent)
// By default, there should be no player state // 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) let audioPlayerState = AudioPlayerState(id: audioPlayerStateId, title: "", duration: 10.0)
mediaPlayerProvider.register(audioPlayerState: audioPlayerState) mediaPlayerProvider.register(audioPlayerState: audioPlayerState)
XCTAssertEqual(audioPlayerState, mediaPlayerProvider.playerState(for: audioPlayerStateId)) #expect(audioPlayerState == mediaPlayerProvider.playerState(for: audioPlayerStateId))
mediaPlayerProvider.unregister(audioPlayerState: audioPlayerState) mediaPlayerProvider.unregister(audioPlayerState: audioPlayerState)
XCTAssertNil(mediaPlayerProvider.playerState(for: audioPlayerStateId)) #expect(mediaPlayerProvider.playerState(for: audioPlayerStateId) == nil)
} }
func testDetachAllStates() { @Test
func detachAllStates() {
let audioPlayer = AudioPlayerMock() let audioPlayer = AudioPlayerMock()
audioPlayer.actions = PassthroughSubject<AudioPlayerAction, Never>().eraseToAnyPublisher() audioPlayer.actions = PassthroughSubject<AudioPlayerAction, Never>().eraseToAnyPublisher()
@@ -45,17 +48,18 @@ class MediaPlayerProviderTests: XCTestCase {
mediaPlayerProvider.register(audioPlayerState: audioPlayerState) mediaPlayerProvider.register(audioPlayerState: audioPlayerState)
audioPlayerState.attachAudioPlayer(audioPlayer) audioPlayerState.attachAudioPlayer(audioPlayer)
let isAttached = audioPlayerState.isAttached let isAttached = audioPlayerState.isAttached
XCTAssertTrue(isAttached) #expect(isAttached)
} }
mediaPlayerProvider.detachAllStates(except: nil) mediaPlayerProvider.detachAllStates(except: nil)
for audioPlayerState in audioPlayerStates { for audioPlayerState in audioPlayerStates {
let isAttached = audioPlayerState.isAttached let isAttached = audioPlayerState.isAttached
XCTAssertFalse(isAttached) #expect(!isAttached)
} }
} }
func testDetachAllStatesWithException() { @Test
func detachAllStatesWithException() {
let audioPlayer = AudioPlayerMock() let audioPlayer = AudioPlayerMock()
audioPlayer.actions = PassthroughSubject<AudioPlayerAction, Never>().eraseToAnyPublisher() audioPlayer.actions = PassthroughSubject<AudioPlayerAction, Never>().eraseToAnyPublisher()
@@ -64,7 +68,7 @@ class MediaPlayerProviderTests: XCTestCase {
mediaPlayerProvider.register(audioPlayerState: audioPlayerState) mediaPlayerProvider.register(audioPlayerState: audioPlayerState)
audioPlayerState.attachAudioPlayer(audioPlayer) audioPlayerState.attachAudioPlayer(audioPlayer)
let isAttached = audioPlayerState.isAttached let isAttached = audioPlayerState.isAttached
XCTAssertTrue(isAttached) #expect(isAttached)
} }
let exception = audioPlayerStates[1] let exception = audioPlayerStates[1]
@@ -72,9 +76,9 @@ class MediaPlayerProviderTests: XCTestCase {
for audioPlayerState in audioPlayerStates { for audioPlayerState in audioPlayerStates {
let isAttached = audioPlayerState.isAttached let isAttached = audioPlayerState.isAttached
if audioPlayerState == exception { if audioPlayerState == exception {
XCTAssertTrue(isAttached) #expect(isAttached)
} else { } else {
XCTAssertFalse(isAttached) #expect(!isAttached)
} }
} }
} }

View File

@@ -7,44 +7,40 @@
// //
@testable import ElementX @testable import ElementX
import Foundation
import MatrixRustSDK import MatrixRustSDK
import MatrixRustSDKMocks import MatrixRustSDKMocks
import XCTest import Testing
final class MediaLoaderTests: XCTestCase { @Suite
func testMediaRequestCoalescing() async throws { struct MediaLoaderTests {
@Test
func mediaRequestCoalescing() async throws {
let mediaLoadingClient = ClientSDKMock() let mediaLoadingClient = ClientSDKMock()
mediaLoadingClient.getMediaContentMediaSourceReturnValue = Data() mediaLoadingClient.getMediaContentMediaSourceReturnValue = Data()
let mediaLoader = MediaLoader(client: mediaLoadingClient) let mediaLoader = MediaLoader(client: mediaLoadingClient)
let mediaSource = try MediaSourceProxy(url: .mockMXCFile, mimeType: nil) let mediaSource = try MediaSourceProxy(url: .mockMXCFile, mimeType: nil)
do { for _ in 1...10 {
for _ in 1...10 { _ = try await mediaLoader.loadMediaContentForSource(mediaSource)
_ = try await mediaLoader.loadMediaContentForSource(mediaSource)
}
XCTAssertEqual(mediaLoadingClient.getMediaContentMediaSourceCallsCount, 10)
} catch {
fatalError()
} }
#expect(mediaLoadingClient.getMediaContentMediaSourceCallsCount == 10)
} }
func testMediaThumbnailRequestCoalescing() async throws { @Test
func mediaThumbnailRequestCoalescing() async throws {
let mediaLoadingClient = ClientSDKMock() let mediaLoadingClient = ClientSDKMock()
mediaLoadingClient.getMediaThumbnailMediaSourceWidthHeightReturnValue = Data() mediaLoadingClient.getMediaThumbnailMediaSourceWidthHeightReturnValue = Data()
let mediaLoader = MediaLoader(client: mediaLoadingClient) let mediaLoader = MediaLoader(client: mediaLoadingClient)
let mediaSource = try MediaSourceProxy(url: .mockMXCImage, mimeType: nil) let mediaSource = try MediaSourceProxy(url: .mockMXCImage, mimeType: nil)
do { for _ in 1...10 {
for _ in 1...10 { _ = try await mediaLoader.loadMediaThumbnailForSource(mediaSource, width: 100, height: 100)
_ = try await mediaLoader.loadMediaThumbnailForSource(mediaSource, width: 100, height: 100)
}
XCTAssertEqual(mediaLoadingClient.getMediaThumbnailMediaSourceWidthHeightCallsCount, 10)
} catch {
fatalError()
} }
#expect(mediaLoadingClient.getMediaThumbnailMediaSourceWidthHeightCallsCount == 10)
} }
} }

View File

@@ -8,20 +8,18 @@
import Combine import Combine
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class MessageForwardingScreenViewModelTests: XCTestCase { @Suite
struct MessageForwardingScreenViewModelTests {
let forwardingItem = MessageForwardingItem(id: .event(uniqueID: .init("t1"), eventOrTransactionID: .eventID("t1")), let forwardingItem = MessageForwardingItem(id: .event(uniqueID: .init("t1"), eventOrTransactionID: .eventID("t1")),
roomID: "1", roomID: "1",
content: .init(noHandle: .init())) content: .init(noHandle: .init()))
var viewModel: MessageForwardingScreenViewModelProtocol! var viewModel: MessageForwardingScreenViewModelProtocol!
var context: MessageForwardingScreenViewModelType.Context! var context: MessageForwardingScreenViewModelType.Context!
var cancellables = Set<AnyCancellable>()
override func setUpWithError() throws {
cancellables.removeAll()
init() {
let clientProxy = ClientProxyMock(.init()) let clientProxy = ClientProxyMock(.init())
clientProxy.roomForIdentifierClosure = { .joined(JoinedRoomProxyMock(.init(id: $0))) } clientProxy.roomForIdentifierClosure = { .joined(JoinedRoomProxyMock(.init(id: $0))) }
@@ -32,45 +30,44 @@ class MessageForwardingScreenViewModelTests: XCTestCase {
context = viewModel.context context = viewModel.context
} }
func testInitialState() { @Test
XCTAssertNil(context.viewState.rooms.first { $0.id == forwardingItem.roomID }, "The source room ID shouldn't be shown") 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")) context.send(viewAction: .selectRoom(roomID: "2"))
XCTAssertEqual(context.viewState.selectedRoomID, "2") #expect(context.viewState.selectedRoomID == "2")
} }
func testSearching() async throws { @Test
let defered = deferFulfillment(context.$viewState) { state in mutating func searching() async throws {
let deferred = deferFulfillment(context.$viewState) { state in
state.rooms.count == 1 state.rooms.count == 1
} }
context.searchQuery = "Second" 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")) context.send(viewAction: .selectRoom(roomID: "2"))
XCTAssertEqual(context.viewState.selectedRoomID, "2") #expect(context.viewState.selectedRoomID == "2")
let expectation = expectation(description: "Wait for confirmation") let deferred = deferFulfillment(viewModel.actions) { action in
switch action {
viewModel.actions case .sent(let roomID):
.sink { action in return roomID == "2"
switch action { default:
case .sent(let roomID): return false
XCTAssertEqual(roomID, "2")
expectation.fulfill()
default:
break
}
} }
.store(in: &cancellables) }
context.send(viewAction: .send) context.send(viewAction: .send)
waitForExpectations(timeout: 5.0) try await deferred.fulfill()
} }
} }

View File

@@ -7,18 +7,21 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Foundation
import Testing
@MainActor @MainActor
class NavigationRootCoordinatorTests: XCTestCase { @Suite
private var navigationRootCoordinator: NavigationRootCoordinator! struct NavigationRootCoordinatorTests {
private var navigationRootCoordinator: NavigationRootCoordinator
override func setUp() { init() {
navigationRootCoordinator = NavigationRootCoordinator() navigationRootCoordinator = NavigationRootCoordinator()
} }
func testRootChanges() { @Test
XCTAssertNil(navigationRootCoordinator.rootCoordinator) func rootChanges() {
#expect(navigationRootCoordinator.rootCoordinator == nil)
let firstRootCoordinator = SomeTestCoordinator() let firstRootCoordinator = SomeTestCoordinator()
navigationRootCoordinator.setRootCoordinator(firstRootCoordinator) navigationRootCoordinator.setRootCoordinator(firstRootCoordinator)
@@ -31,7 +34,8 @@ class NavigationRootCoordinatorTests: XCTestCase {
assertCoordinatorsEqual(secondRootCoordinator, navigationRootCoordinator.rootCoordinator) assertCoordinatorsEqual(secondRootCoordinator, navigationRootCoordinator.rootCoordinator)
} }
func testOverlay() { @Test
func overlay() {
let rootCoordinator = SomeTestCoordinator() let rootCoordinator = SomeTestCoordinator()
navigationRootCoordinator.setRootCoordinator(rootCoordinator) navigationRootCoordinator.setRootCoordinator(rootCoordinator)
@@ -44,35 +48,37 @@ class NavigationRootCoordinatorTests: XCTestCase {
navigationRootCoordinator.setOverlayCoordinator(nil) navigationRootCoordinator.setOverlayCoordinator(nil)
assertCoordinatorsEqual(rootCoordinator, navigationRootCoordinator.rootCoordinator) assertCoordinatorsEqual(rootCoordinator, navigationRootCoordinator.rootCoordinator)
XCTAssertNil(navigationRootCoordinator.overlayCoordinator) #expect(navigationRootCoordinator.overlayCoordinator == nil)
} }
// MARK: - Dismissal Callbacks // MARK: - Dismissal Callbacks
func testReplacementDismissalCallbacks() { @Test
XCTAssertNil(navigationRootCoordinator.rootCoordinator) func replacementDismissalCallbacks() async {
#expect(navigationRootCoordinator.rootCoordinator == nil)
let rootCoordinator = SomeTestCoordinator() let rootCoordinator = SomeTestCoordinator()
let expectation = expectation(description: "Wait for callback") await confirmation("Wait for callback") { confirm in
navigationRootCoordinator.setRootCoordinator(rootCoordinator) { navigationRootCoordinator.setRootCoordinator(rootCoordinator) {
expectation.fulfill() confirm()
} }
navigationRootCoordinator.setRootCoordinator(nil) navigationRootCoordinator.setRootCoordinator(nil)
waitForExpectations(timeout: 1.0) }
} }
func testOverlayDismissalCallback() { @Test
func overlayDismissalCallback() async {
let overlayCoordinator = SomeTestCoordinator() let overlayCoordinator = SomeTestCoordinator()
let expectation = expectation(description: "Wait for callback") await confirmation("Wait for callback") { confirm in
navigationRootCoordinator.setOverlayCoordinator(overlayCoordinator) { navigationRootCoordinator.setOverlayCoordinator(overlayCoordinator) {
expectation.fulfill() confirm()
} }
navigationRootCoordinator.setOverlayCoordinator(nil) navigationRootCoordinator.setOverlayCoordinator(nil)
waitForExpectations(timeout: 1.0) }
} }
// MARK: - Private // MARK: - Private
@@ -80,11 +86,11 @@ class NavigationRootCoordinatorTests: XCTestCase {
private func assertCoordinatorsEqual(_ lhs: CoordinatorProtocol?, _ rhs: CoordinatorProtocol?) { private func assertCoordinatorsEqual(_ lhs: CoordinatorProtocol?, _ rhs: CoordinatorProtocol?) {
guard let lhs = lhs as? SomeTestCoordinator, guard let lhs = lhs as? SomeTestCoordinator,
let rhs = rhs as? SomeTestCoordinator else { let rhs = rhs as? SomeTestCoordinator else {
XCTFail("Coordinators are not the same") Issue.record("Coordinators are not the same")
return return
} }
XCTAssertEqual(lhs.id, rhs.id) #expect(lhs.id == rhs.id)
} }
} }

View File

@@ -7,18 +7,21 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Foundation
import Testing
@MainActor @MainActor
class NavigationStackCoordinatorTests: XCTestCase { @Suite
private var navigationStackCoordinator: NavigationStackCoordinator! struct NavigationStackCoordinatorTests {
private var navigationStackCoordinator: NavigationStackCoordinator
override func setUp() { init() {
navigationStackCoordinator = NavigationStackCoordinator() navigationStackCoordinator = NavigationStackCoordinator()
} }
func testRoot() { @Test
XCTAssertNil(navigationStackCoordinator.rootCoordinator) func root() {
#expect(navigationStackCoordinator.rootCoordinator == nil)
let rootCoordinator = SomeTestCoordinator() let rootCoordinator = SomeTestCoordinator()
navigationStackCoordinator.setRootCoordinator(rootCoordinator) navigationStackCoordinator.setRootCoordinator(rootCoordinator)
@@ -26,7 +29,8 @@ class NavigationStackCoordinatorTests: XCTestCase {
assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator) assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator)
} }
func testSingleSheet() { @Test
mutating func singleSheet() {
let rootCoordinator = SomeTestCoordinator() let rootCoordinator = SomeTestCoordinator()
navigationStackCoordinator.setRootCoordinator(rootCoordinator) navigationStackCoordinator.setRootCoordinator(rootCoordinator)
@@ -39,10 +43,11 @@ class NavigationStackCoordinatorTests: XCTestCase {
navigationStackCoordinator.setSheetCoordinator(nil) navigationStackCoordinator.setSheetCoordinator(nil)
assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator) assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator)
XCTAssertNil(navigationStackCoordinator.sheetCoordinator) #expect(navigationStackCoordinator.sheetCoordinator == nil)
} }
func testMultipleSheets() { @Test
mutating func multipleSheets() {
let rootCoordinator = SomeTestCoordinator() let rootCoordinator = SomeTestCoordinator()
navigationStackCoordinator.setRootCoordinator(rootCoordinator) navigationStackCoordinator.setRootCoordinator(rootCoordinator)
@@ -50,18 +55,19 @@ class NavigationStackCoordinatorTests: XCTestCase {
navigationStackCoordinator.setSheetCoordinator(sheetCoordinator) navigationStackCoordinator.setSheetCoordinator(sheetCoordinator)
assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator) assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator)
XCTAssert(navigationStackCoordinator.stackCoordinators.isEmpty) #expect(navigationStackCoordinator.stackCoordinators.isEmpty)
assertCoordinatorsEqual(sheetCoordinator, navigationStackCoordinator.sheetCoordinator) assertCoordinatorsEqual(sheetCoordinator, navigationStackCoordinator.sheetCoordinator)
let someOtherSheetCoordinator = SomeTestCoordinator() let someOtherSheetCoordinator = SomeTestCoordinator()
navigationStackCoordinator.setSheetCoordinator(someOtherSheetCoordinator) navigationStackCoordinator.setSheetCoordinator(someOtherSheetCoordinator)
assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator) assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator)
XCTAssert(navigationStackCoordinator.stackCoordinators.isEmpty) #expect(navigationStackCoordinator.stackCoordinators.isEmpty)
assertCoordinatorsEqual(someOtherSheetCoordinator, navigationStackCoordinator.sheetCoordinator) assertCoordinatorsEqual(someOtherSheetCoordinator, navigationStackCoordinator.sheetCoordinator)
} }
func testSinglePush() { @Test
mutating func singlePush() {
let rootCoordinator = SomeTestCoordinator() let rootCoordinator = SomeTestCoordinator()
navigationStackCoordinator.setRootCoordinator(rootCoordinator) navigationStackCoordinator.setRootCoordinator(rootCoordinator)
@@ -74,10 +80,11 @@ class NavigationStackCoordinatorTests: XCTestCase {
navigationStackCoordinator.pop() navigationStackCoordinator.pop()
assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator) assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator)
XCTAssert(navigationStackCoordinator.stackCoordinators.isEmpty) #expect(navigationStackCoordinator.stackCoordinators.isEmpty)
} }
func testMultiplePushes() { @Test
mutating func multiplePushes() {
let rootCoordinator = SomeTestCoordinator() let rootCoordinator = SomeTestCoordinator()
navigationStackCoordinator.setRootCoordinator(rootCoordinator) navigationStackCoordinator.setRootCoordinator(rootCoordinator)
@@ -89,7 +96,7 @@ class NavigationStackCoordinatorTests: XCTestCase {
} }
assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator) assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator)
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, coordinators.count) #expect(navigationStackCoordinator.stackCoordinators.count == coordinators.count)
for index in coordinators.indices { for index in coordinators.indices {
assertCoordinatorsEqual(coordinators[index], navigationStackCoordinator.stackCoordinators[index]) assertCoordinatorsEqual(coordinators[index], navigationStackCoordinator.stackCoordinators[index])
@@ -98,10 +105,11 @@ class NavigationStackCoordinatorTests: XCTestCase {
navigationStackCoordinator.popToRoot() navigationStackCoordinator.popToRoot()
assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator) assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator)
XCTAssert(navigationStackCoordinator.stackCoordinators.isEmpty) #expect(navigationStackCoordinator.stackCoordinators.isEmpty)
} }
func testRootReplacementDimissesTheRest() { @Test
mutating func rootReplacementDimissesTheRest() {
let rootCoordinator = SomeTestCoordinator() let rootCoordinator = SomeTestCoordinator()
navigationStackCoordinator.setRootCoordinator(rootCoordinator) navigationStackCoordinator.setRootCoordinator(rootCoordinator)
@@ -119,10 +127,11 @@ class NavigationStackCoordinatorTests: XCTestCase {
navigationStackCoordinator.setRootCoordinator(newRootCoordinator) navigationStackCoordinator.setRootCoordinator(newRootCoordinator)
assertCoordinatorsEqual(newRootCoordinator, navigationStackCoordinator.rootCoordinator) assertCoordinatorsEqual(newRootCoordinator, navigationStackCoordinator.rootCoordinator)
XCTAssert(navigationStackCoordinator.stackCoordinators.isEmpty) #expect(navigationStackCoordinator.stackCoordinators.isEmpty)
} }
func testPushesDontReplaceSheet() { @Test
mutating func pushesDontReplaceSheet() {
let sheetCoordinator = SomeTestCoordinator() let sheetCoordinator = SomeTestCoordinator()
navigationStackCoordinator.setSheetCoordinator(sheetCoordinator) navigationStackCoordinator.setSheetCoordinator(sheetCoordinator)
@@ -142,54 +151,57 @@ class NavigationStackCoordinatorTests: XCTestCase {
// MARK: - Dismissal Callbacks // MARK: - Dismissal Callbacks
func testPopDismissalCallbacks() { @Test
mutating func popDismissalCallbacks() async {
let pushedCoordinator = SomeTestCoordinator() let pushedCoordinator = SomeTestCoordinator()
let expectation = expectation(description: "Wait for callback") await confirmation("Wait for callback") { confirm in
navigationStackCoordinator.push(pushedCoordinator) { navigationStackCoordinator.push(pushedCoordinator) {
expectation.fulfill() confirm()
} }
navigationStackCoordinator.pop() navigationStackCoordinator.pop()
waitForExpectations(timeout: 1.0) }
} }
func testPopToRootDismissalCallbacks() { @Test
mutating func popToRootDismissalCallbacks() async {
navigationStackCoordinator.push(SomeTestCoordinator()) navigationStackCoordinator.push(SomeTestCoordinator())
navigationStackCoordinator.push(SomeTestCoordinator()) navigationStackCoordinator.push(SomeTestCoordinator())
let coordinator = SomeTestCoordinator() let coordinator = SomeTestCoordinator()
let expectation = expectation(description: "Wait for callback") await confirmation("Wait for callback") { confirm in
navigationStackCoordinator.push(coordinator) { navigationStackCoordinator.push(coordinator) {
expectation.fulfill() confirm()
} }
navigationStackCoordinator.popToRoot() navigationStackCoordinator.popToRoot()
waitForExpectations(timeout: 1.0) }
} }
func testSheetDismissalCallback() { @Test
mutating func sheetDismissalCallback() async {
let coordinator = SomeTestCoordinator() let coordinator = SomeTestCoordinator()
let expectation = expectation(description: "Wait for callback") await confirmation("Wait for callback") { confirm in
navigationStackCoordinator.setSheetCoordinator(coordinator) { navigationStackCoordinator.setSheetCoordinator(coordinator) {
expectation.fulfill() confirm()
} }
navigationStackCoordinator.setSheetCoordinator(nil) navigationStackCoordinator.setSheetCoordinator(nil)
waitForExpectations(timeout: 1.0) }
} }
func testRootReplacementCallbacks() { @Test
mutating func rootReplacementCallbacks() async {
navigationStackCoordinator.setRootCoordinator(SomeTestCoordinator()) navigationStackCoordinator.setRootCoordinator(SomeTestCoordinator())
let popExpectation = expectation(description: "Waiting for callback") await confirmation("Waiting for callback") { confirm in
navigationStackCoordinator.push(SomeTestCoordinator()) { navigationStackCoordinator.push(SomeTestCoordinator()) {
popExpectation.fulfill() confirm()
}
navigationStackCoordinator.setRootCoordinator(SomeTestCoordinator())
} }
navigationStackCoordinator.setRootCoordinator(SomeTestCoordinator())
waitForExpectations(timeout: 1.0)
} }
// MARK: - Private // MARK: - Private
@@ -197,11 +209,11 @@ class NavigationStackCoordinatorTests: XCTestCase {
private func assertCoordinatorsEqual(_ lhs: CoordinatorProtocol?, _ rhs: CoordinatorProtocol?) { private func assertCoordinatorsEqual(_ lhs: CoordinatorProtocol?, _ rhs: CoordinatorProtocol?) {
guard let lhs = lhs as? SomeTestCoordinator, guard let lhs = lhs as? SomeTestCoordinator,
let rhs = rhs as? SomeTestCoordinator else { let rhs = rhs as? SomeTestCoordinator else {
XCTFail("Coordinators are not the same") Issue.record("Coordinators are not the same")
return return
} }
XCTAssertEqual(lhs.id, rhs.id) #expect(lhs.id == rhs.id)
} }
} }

View File

@@ -7,19 +7,22 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Foundation
import Testing
@MainActor @MainActor
class NavigationTabCoordinatorTests: XCTestCase { @Suite
struct NavigationTabCoordinatorTests {
enum TestTab { case tab, chats, spaces } enum TestTab { case tab, chats, spaces }
private var navigationTabCoordinator: NavigationTabCoordinator<TestTab>! private var navigationTabCoordinator: NavigationTabCoordinator<TestTab>
override func setUp() { init() {
navigationTabCoordinator = NavigationTabCoordinator() navigationTabCoordinator = NavigationTabCoordinator()
} }
func testTabs() { @Test
XCTAssertTrue(navigationTabCoordinator.tabCoordinators.isEmpty) mutating func tabs() {
#expect(navigationTabCoordinator.tabCoordinators.isEmpty)
let someCoordinator = SomeTestCoordinator() let someCoordinator = SomeTestCoordinator()
navigationTabCoordinator.setTabs([.init(coordinator: someCoordinator, details: .init(tag: .tab, title: "Tab", icon: \.help, selectedIcon: \.helpSolid))]) 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]) assertCoordinatorsEqual(navigationTabCoordinator.tabCoordinators, [chatsCoordinator, spacesCoordinator])
} }
func testSingleSheet() { @Test
mutating func singleSheet() {
let tabCoordinator = SomeTestCoordinator() let tabCoordinator = SomeTestCoordinator()
navigationTabCoordinator.setTabs([.init(coordinator: tabCoordinator, details: .init(tag: .tab, title: "Tab", icon: \.help, selectedIcon: \.helpSolid))]) 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) navigationTabCoordinator.setSheetCoordinator(nil)
assertCoordinatorsEqual(navigationTabCoordinator.tabCoordinators, [tabCoordinator]) assertCoordinatorsEqual(navigationTabCoordinator.tabCoordinators, [tabCoordinator])
XCTAssertNil(navigationTabCoordinator.sheetCoordinator) #expect(navigationTabCoordinator.sheetCoordinator == nil)
} }
func testMultipleSheets() { @Test
mutating func multipleSheets() {
let tabCoordinator = SomeTestCoordinator() let tabCoordinator = SomeTestCoordinator()
navigationTabCoordinator.setTabs([.init(coordinator: tabCoordinator, details: .init(tag: .tab, title: "Tab", icon: \.help, selectedIcon: \.helpSolid))]) 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) assertCoordinatorsEqual(someOtherSheetCoordinator, navigationTabCoordinator.sheetCoordinator)
} }
func testFullScreenCover() { @Test
mutating func fullScreenCover() {
let tabCoordinator = SomeTestCoordinator() let tabCoordinator = SomeTestCoordinator()
navigationTabCoordinator.setTabs([.init(coordinator: tabCoordinator, details: .init(tag: .tab, title: "Tab", icon: \.help, selectedIcon: \.helpSolid))]) 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) navigationTabCoordinator.setFullScreenCoverCoordinator(nil)
assertCoordinatorsEqual(navigationTabCoordinator.tabCoordinators, [tabCoordinator]) assertCoordinatorsEqual(navigationTabCoordinator.tabCoordinators, [tabCoordinator])
XCTAssertNil(navigationTabCoordinator.fullScreenCoverCoordinator) #expect(navigationTabCoordinator.fullScreenCoverCoordinator == nil)
} }
func testOverlay() { @Test
mutating func overlay() {
let tabCoordinator = SomeTestCoordinator() let tabCoordinator = SomeTestCoordinator()
navigationTabCoordinator.setTabs([.init(coordinator: tabCoordinator, details: .init(tag: .tab, title: "Tab", icon: \.help, selectedIcon: \.helpSolid))]) 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) navigationTabCoordinator.setOverlayCoordinator(nil)
assertCoordinatorsEqual(navigationTabCoordinator.tabCoordinators, [tabCoordinator]) assertCoordinatorsEqual(navigationTabCoordinator.tabCoordinators, [tabCoordinator])
XCTAssertNil(navigationTabCoordinator.overlayCoordinator) #expect(navigationTabCoordinator.overlayCoordinator == nil)
} }
// MARK: - Dismissal Callbacks // MARK: - Dismissal Callbacks
func testTabDismissalCallbacks() { @Test
mutating func tabDismissalCallbacks() async {
let chatsCoordinator = SomeTestCoordinator() let chatsCoordinator = SomeTestCoordinator()
let spacesCoordinator = SomeTestCoordinator() let spacesCoordinator = SomeTestCoordinator()
let expectation = expectation(description: "Wait for callback") await confirmation("Wait for callback", expectedCount: 2) { confirm in
expectation.expectedFulfillmentCount = 2 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([ navigationTabCoordinator.setTabs([.init(coordinator: SomeTestCoordinator(), details: .init(tag: .tab, title: "Tab", icon: \.help, selectedIcon: \.helpSolid))])
.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()
} }
navigationTabCoordinator.setSheetCoordinator(nil)
waitForExpectations(timeout: 1.0)
} }
func testFullScreenCoverDismissalCallback() { @Test
mutating func sheetDismissalCallback() async {
let coordinator = SomeTestCoordinator() let coordinator = SomeTestCoordinator()
let expectation = expectation(description: "Wait for callback") await confirmation("Wait for callback") { confirm in
navigationTabCoordinator.setFullScreenCoverCoordinator(coordinator) { navigationTabCoordinator.setSheetCoordinator(coordinator) {
expectation.fulfill() confirm()
} }
navigationTabCoordinator.setFullScreenCoverCoordinator(nil) navigationTabCoordinator.setSheetCoordinator(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 overlayCoordinator = SomeTestCoordinator()
let expectation = expectation(description: "Wait for callback") await confirmation("Wait for callback") { confirm in
navigationTabCoordinator.setOverlayCoordinator(overlayCoordinator) { navigationTabCoordinator.setOverlayCoordinator(overlayCoordinator) {
expectation.fulfill() confirm()
} }
navigationTabCoordinator.setOverlayCoordinator(nil) navigationTabCoordinator.setOverlayCoordinator(nil)
waitForExpectations(timeout: 1.0) }
} }
func testOverlayDismissalCallbackWhenChangingMode() { @Test
mutating func overlayDismissalCallbackWhenChangingMode() async throws {
let overlayCoordinator = SomeTestCoordinator() let overlayCoordinator = SomeTestCoordinator()
let expectation = expectation(description: "Wait for callback") try await confirmation("Callback should not be called when just changing mode",
expectation.isInverted = true expectedCount: 0) { confirmation in
navigationTabCoordinator.setOverlayCoordinator(overlayCoordinator) { navigationTabCoordinator.setOverlayCoordinator(overlayCoordinator) {
expectation.fulfill() confirmation()
} }
navigationTabCoordinator.setOverlayPresentationMode(.minimized) navigationTabCoordinator.setOverlayPresentationMode(.minimized)
waitForExpectations(timeout: 1.0) try await Task.sleep(for: .seconds(1))
}
} }
// MARK: - Private // MARK: - Private
@@ -176,16 +187,16 @@ class NavigationTabCoordinatorTests: XCTestCase {
private func assertCoordinatorsEqual(_ lhs: CoordinatorProtocol?, _ rhs: CoordinatorProtocol?) { private func assertCoordinatorsEqual(_ lhs: CoordinatorProtocol?, _ rhs: CoordinatorProtocol?) {
guard let lhs = lhs as? SomeTestCoordinator, guard let lhs = lhs as? SomeTestCoordinator,
let rhs = rhs as? SomeTestCoordinator else { let rhs = rhs as? SomeTestCoordinator else {
XCTFail("Coordinators are not the same") Issue.record("Coordinators are not the same")
return return
} }
XCTAssertEqual(lhs.id, rhs.id) #expect(lhs.id == rhs.id)
} }
private func assertCoordinatorsEqual(_ lhs: [CoordinatorProtocol], _ rhs: [CoordinatorProtocol]) { private func assertCoordinatorsEqual(_ lhs: [CoordinatorProtocol], _ rhs: [CoordinatorProtocol]) {
guard lhs.count == rhs.count else { guard lhs.count == rhs.count else {
XCTFail("Coordinators are not the same") Issue.record("Coordinators are not the same")
return return
} }

View File

@@ -8,14 +8,16 @@
import Dynamic import Dynamic
@testable import ElementX @testable import ElementX
import MatrixRustSDK import MatrixRustSDK
import XCTest import Testing
import UserNotifications
final class NotificationContentBuilderTests: XCTestCase { @Suite
var notificationContentBuilder: NotificationContentBuilder! struct NotificationContentBuilderTests {
var mediaProvider: MediaProviderMock! var notificationContentBuilder: NotificationContentBuilder
var notificationContent: UNMutableNotificationContent! var mediaProvider: MediaProviderMock
var notificationContent: UNMutableNotificationContent
override func setUp() { init() {
notificationContent = .init() notificationContent = .init()
let stringBuilder = RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(mentionBuilder: PlainMentionBuilder()), let stringBuilder = RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(mentionBuilder: PlainMentionBuilder()),
destination: .notification) destination: .notification)
@@ -25,7 +27,8 @@ final class NotificationContentBuilderTests: XCTestCase {
userSession: NSEUserSessionMock(.init())) userSession: NSEUserSessionMock(.init()))
} }
func testDMMessageNotification() async { @Test
mutating func dmMessageNotification() async {
let notificationItem = NotificationItemProxyMock(.init(roomID: "!test:matrix.org", let notificationItem = NotificationItemProxyMock(.init(roomID: "!test:matrix.org",
receiverID: "@bob:matrix.org", receiverID: "@bob:matrix.org",
senderDisplayName: "Alice", senderDisplayName: "Alice",
@@ -40,18 +43,19 @@ final class NotificationContentBuilderTests: XCTestCase {
let communicationContext = Dynamic(notificationContent, memberName: "communicationContext") let communicationContext = Dynamic(notificationContent, memberName: "communicationContext")
// Checking if nil without using asObject always fails // Checking if nil without using asObject always fails
XCTAssertNil(communicationContext.displayName.asObject) #expect(communicationContext.displayName.asObject == nil)
XCTAssertEqual(communicationContext.sender.displayName, "Alice") #expect(communicationContext.sender.displayName == "Alice")
XCTAssertEqual(notificationContent.body, "Hello world!") #expect(notificationContent.body == "Hello world!")
XCTAssertEqual(notificationContent.categoryIdentifier, NotificationConstants.Category.message) #expect(notificationContent.categoryIdentifier == NotificationConstants.Category.message)
XCTAssertNil(notificationContent.threadRootEventID) #expect(notificationContent.threadRootEventID == nil)
XCTAssertNotNil(notificationContent.sound) #expect(notificationContent.sound != nil)
// Remember we remove the @ due to an iOS bug // Remember we remove the @ due to an iOS bug
XCTAssertEqual(notificationContent.threadIdentifier, "bob:matrix.org!test:matrix.org") #expect(notificationContent.threadIdentifier == "bob:matrix.org!test:matrix.org")
XCTAssertEqual(notificationContent.attachments, []) #expect(notificationContent.attachments == [])
} }
func testDMMessageNotificationWithMention() async { @Test
mutating func dmMessageNotificationWithMention() async {
let notificationItem = NotificationItemProxyMock(.init(roomID: "!test:matrix.org", let notificationItem = NotificationItemProxyMock(.init(roomID: "!test:matrix.org",
receiverID: "@bob:matrix.org", receiverID: "@bob:matrix.org",
senderDisplayName: "Alice", senderDisplayName: "Alice",
@@ -68,18 +72,19 @@ final class NotificationContentBuilderTests: XCTestCase {
let communicationContext = Dynamic(notificationContent, memberName: "communicationContext") let communicationContext = Dynamic(notificationContent, memberName: "communicationContext")
// Checking if nil without using asObject always fails // Checking if nil without using asObject always fails
XCTAssertNil(communicationContext.displayName.asObject) #expect(communicationContext.displayName.asObject == nil)
XCTAssertEqual(communicationContext.sender.displayName, L10n.notificationSenderMentionReply("Alice")) #expect(communicationContext.sender.displayName == L10n.notificationSenderMentionReply("Alice"))
XCTAssertEqual(notificationContent.body, "Hello world!") #expect(notificationContent.body == "Hello world!")
XCTAssertEqual(notificationContent.categoryIdentifier, NotificationConstants.Category.message) #expect(notificationContent.categoryIdentifier == NotificationConstants.Category.message)
XCTAssertNil(notificationContent.threadRootEventID) #expect(notificationContent.threadRootEventID == nil)
XCTAssertNotNil(notificationContent.sound) #expect(notificationContent.sound != nil)
// Remember we remove the @ due to an iOS bug // Remember we remove the @ due to an iOS bug
XCTAssertEqual(notificationContent.threadIdentifier, "bob:matrix.org!test:matrix.org") #expect(notificationContent.threadIdentifier == "bob:matrix.org!test:matrix.org")
XCTAssertEqual(notificationContent.attachments, []) #expect(notificationContent.attachments == [])
} }
func testDMMessageNotificationWithThread() async { @Test
mutating func dmMessageNotificationWithThread() async {
let notificationItem = NotificationItemProxyMock(.init(roomID: "!test:matrix.org", let notificationItem = NotificationItemProxyMock(.init(roomID: "!test:matrix.org",
receiverID: "@bob:matrix.org", receiverID: "@bob:matrix.org",
senderDisplayName: "Alice", senderDisplayName: "Alice",
@@ -96,18 +101,19 @@ final class NotificationContentBuilderTests: XCTestCase {
mediaProvider: mediaProvider) mediaProvider: mediaProvider)
let communicationContext = Dynamic(notificationContent, memberName: "communicationContext") let communicationContext = Dynamic(notificationContent, memberName: "communicationContext")
XCTAssertEqual(communicationContext.displayName, L10n.commonThread) #expect(communicationContext.displayName == L10n.commonThread)
XCTAssertEqual(communicationContext.sender.displayName, "Alice") #expect(communicationContext.sender.displayName == "Alice")
XCTAssertEqual(notificationContent.body, "Hello world!") #expect(notificationContent.body == "Hello world!")
XCTAssertEqual(notificationContent.categoryIdentifier, NotificationConstants.Category.message) #expect(notificationContent.categoryIdentifier == NotificationConstants.Category.message)
XCTAssertNotNil(notificationContent.threadRootEventID) #expect(notificationContent.threadRootEventID != nil)
XCTAssertNotNil(notificationContent.sound) #expect(notificationContent.sound != nil)
// Remember we remove the @ due to an iOS bug // Remember we remove the @ due to an iOS bug
XCTAssertEqual(notificationContent.threadIdentifier, "bob:matrix.org!test:matrix.orgthread") #expect(notificationContent.threadIdentifier == "bob:matrix.org!test:matrix.orgthread")
XCTAssertEqual(notificationContent.attachments, []) #expect(notificationContent.attachments == [])
} }
func testDMMessageNotificationWithThreadAndMention() async { @Test
mutating func dmMessageNotificationWithThreadAndMention() async {
let notificationItem = NotificationItemProxyMock(.init(roomID: "!test:matrix.org", let notificationItem = NotificationItemProxyMock(.init(roomID: "!test:matrix.org",
receiverID: "@bob:matrix.org", receiverID: "@bob:matrix.org",
senderDisplayName: "Alice", senderDisplayName: "Alice",
@@ -124,18 +130,19 @@ final class NotificationContentBuilderTests: XCTestCase {
mediaProvider: mediaProvider) mediaProvider: mediaProvider)
let communicationContext = Dynamic(notificationContent, memberName: "communicationContext") let communicationContext = Dynamic(notificationContent, memberName: "communicationContext")
XCTAssertEqual(communicationContext.displayName, L10n.commonThread) #expect(communicationContext.displayName == L10n.commonThread)
XCTAssertEqual(communicationContext.sender.displayName, L10n.notificationSenderMentionReply("Alice")) #expect(communicationContext.sender.displayName == L10n.notificationSenderMentionReply("Alice"))
XCTAssertEqual(notificationContent.body, "Hello world!") #expect(notificationContent.body == "Hello world!")
XCTAssertEqual(notificationContent.categoryIdentifier, NotificationConstants.Category.message) #expect(notificationContent.categoryIdentifier == NotificationConstants.Category.message)
XCTAssertNotNil(notificationContent.threadRootEventID) #expect(notificationContent.threadRootEventID != nil)
XCTAssertNotNil(notificationContent.sound) #expect(notificationContent.sound != nil)
// Remember we remove the @ due to an iOS bug // Remember we remove the @ due to an iOS bug
XCTAssertEqual(notificationContent.threadIdentifier, "bob:matrix.org!test:matrix.orgthread") #expect(notificationContent.threadIdentifier == "bob:matrix.org!test:matrix.orgthread")
XCTAssertEqual(notificationContent.attachments, []) #expect(notificationContent.attachments == [])
} }
func testRoomMessageNotification() async { @Test
mutating func roomMessageNotification() async {
let notificationItem = NotificationItemProxyMock(.init(roomID: "!testroom:matrix.org", let notificationItem = NotificationItemProxyMock(.init(roomID: "!testroom:matrix.org",
receiverID: "@bob:matrix.org", receiverID: "@bob:matrix.org",
senderDisplayName: "Alice", senderDisplayName: "Alice",
@@ -150,18 +157,19 @@ final class NotificationContentBuilderTests: XCTestCase {
mediaProvider: mediaProvider) mediaProvider: mediaProvider)
let communicationContext = Dynamic(notificationContent, memberName: "communicationContext") let communicationContext = Dynamic(notificationContent, memberName: "communicationContext")
XCTAssertEqual(communicationContext.displayName, "General") #expect(communicationContext.displayName == "General")
XCTAssertEqual(communicationContext.sender.displayName, "Alice") #expect(communicationContext.sender.displayName == "Alice")
XCTAssertEqual(notificationContent.body, "Hello world!") #expect(notificationContent.body == "Hello world!")
XCTAssertEqual(notificationContent.categoryIdentifier, NotificationConstants.Category.message) #expect(notificationContent.categoryIdentifier == NotificationConstants.Category.message)
XCTAssertNil(notificationContent.threadRootEventID) #expect(notificationContent.threadRootEventID == nil)
XCTAssertNil(notificationContent.sound) #expect(notificationContent.sound == nil)
// Remember we remove the @ due to an iOS bug // Remember we remove the @ due to an iOS bug
XCTAssertEqual(notificationContent.threadIdentifier, "bob:matrix.org!testroom:matrix.org") #expect(notificationContent.threadIdentifier == "bob:matrix.org!testroom:matrix.org")
XCTAssertEqual(notificationContent.attachments, []) #expect(notificationContent.attachments == [])
} }
func testRoomMessageNotificationWithMention() async { @Test
mutating func roomMessageNotificationWithMention() async {
let notificationItem = NotificationItemProxyMock(.init(roomID: "!testroom:matrix.org", let notificationItem = NotificationItemProxyMock(.init(roomID: "!testroom:matrix.org",
receiverID: "@bob:matrix.org", receiverID: "@bob:matrix.org",
senderDisplayName: "Alice", senderDisplayName: "Alice",
@@ -177,17 +185,18 @@ final class NotificationContentBuilderTests: XCTestCase {
mediaProvider: mediaProvider) mediaProvider: mediaProvider)
let communicationContext = Dynamic(notificationContent, memberName: "communicationContext") let communicationContext = Dynamic(notificationContent, memberName: "communicationContext")
XCTAssertEqual(communicationContext.displayName, "General") #expect(communicationContext.displayName == "General")
XCTAssertEqual(communicationContext.sender.displayName, L10n.notificationSenderMentionReply("Alice")) #expect(communicationContext.sender.displayName == L10n.notificationSenderMentionReply("Alice"))
XCTAssertEqual(notificationContent.body, "Hello world!") #expect(notificationContent.body == "Hello world!")
XCTAssertEqual(notificationContent.categoryIdentifier, NotificationConstants.Category.message) #expect(notificationContent.categoryIdentifier == NotificationConstants.Category.message)
XCTAssertNil(notificationContent.threadRootEventID) #expect(notificationContent.threadRootEventID == nil)
XCTAssertNotNil(notificationContent.sound) #expect(notificationContent.sound != nil)
XCTAssertEqual(notificationContent.threadIdentifier, "bob:matrix.org!testroom:matrix.org") #expect(notificationContent.threadIdentifier == "bob:matrix.org!testroom:matrix.org")
XCTAssertEqual(notificationContent.attachments, []) #expect(notificationContent.attachments == [])
} }
func testRoomMessageNotificationWithThread() async { @Test
mutating func roomMessageNotificationWithThread() async {
let notificationItem = NotificationItemProxyMock(.init(roomID: "!testroom:matrix.org", let notificationItem = NotificationItemProxyMock(.init(roomID: "!testroom:matrix.org",
receiverID: "@bob:matrix.org", receiverID: "@bob:matrix.org",
senderDisplayName: "Alice", senderDisplayName: "Alice",
@@ -203,17 +212,18 @@ final class NotificationContentBuilderTests: XCTestCase {
mediaProvider: mediaProvider) mediaProvider: mediaProvider)
let communicationContext = Dynamic(notificationContent, memberName: "communicationContext") let communicationContext = Dynamic(notificationContent, memberName: "communicationContext")
XCTAssertEqual(communicationContext.displayName, L10n.notificationThreadInRoom("General")) #expect(communicationContext.displayName == L10n.notificationThreadInRoom("General"))
XCTAssertEqual(communicationContext.sender.displayName, "Alice") #expect(communicationContext.sender.displayName == "Alice")
XCTAssertEqual(notificationContent.body, "Hello world!") #expect(notificationContent.body == "Hello world!")
XCTAssertEqual(notificationContent.categoryIdentifier, NotificationConstants.Category.message) #expect(notificationContent.categoryIdentifier == NotificationConstants.Category.message)
XCTAssertNotNil(notificationContent.threadRootEventID) #expect(notificationContent.threadRootEventID != nil)
XCTAssertNil(notificationContent.sound) #expect(notificationContent.sound == nil)
XCTAssertEqual(notificationContent.threadIdentifier, "bob:matrix.org!testroom:matrix.orgthread123") #expect(notificationContent.threadIdentifier == "bob:matrix.org!testroom:matrix.orgthread123")
XCTAssertEqual(notificationContent.attachments, []) #expect(notificationContent.attachments == [])
} }
func testRoomMessageNotificationWithThreadAndMention() async { @Test
mutating func roomMessageNotificationWithThreadAndMention() async {
let notificationItem = NotificationItemProxyMock(.init(roomID: "!testroom:matrix.org", let notificationItem = NotificationItemProxyMock(.init(roomID: "!testroom:matrix.org",
receiverID: "@bob:matrix.org", receiverID: "@bob:matrix.org",
senderDisplayName: "Alice", senderDisplayName: "Alice",
@@ -228,13 +238,13 @@ final class NotificationContentBuilderTests: XCTestCase {
notificationItem: notificationItem, notificationItem: notificationItem,
mediaProvider: mediaProvider) mediaProvider: mediaProvider)
let communicationContext = Dynamic(notificationContent, memberName: "communicationContext") let communicationContext = Dynamic(notificationContent, memberName: "communicationContext")
XCTAssertEqual(communicationContext.displayName, L10n.notificationThreadInRoom("General")) #expect(communicationContext.displayName == L10n.notificationThreadInRoom("General"))
XCTAssertEqual(communicationContext.sender.displayName, L10n.notificationSenderMentionReply("Alice")) #expect(communicationContext.sender.displayName == L10n.notificationSenderMentionReply("Alice"))
XCTAssertEqual(notificationContent.body, "Hello world!") #expect(notificationContent.body == "Hello world!")
XCTAssertEqual(notificationContent.categoryIdentifier, NotificationConstants.Category.message) #expect(notificationContent.categoryIdentifier == NotificationConstants.Category.message)
XCTAssertNotNil(notificationContent.threadRootEventID) #expect(notificationContent.threadRootEventID != nil)
XCTAssertNotNil(notificationContent.sound) #expect(notificationContent.sound != nil)
XCTAssertEqual(notificationContent.threadIdentifier, "bob:matrix.org!testroom:matrix.orgthread123") #expect(notificationContent.threadIdentifier == "bob:matrix.org!testroom:matrix.orgthread123")
XCTAssertEqual(notificationContent.attachments, []) #expect(notificationContent.attachments == [])
} }
} }

View File

@@ -7,46 +7,50 @@
// //
@testable import ElementX @testable import ElementX
import Foundation
import MatrixRustSDK import MatrixRustSDK
import XCTest import Testing
/// Just for API sanity checking, they're already properly tested in the SDK/Ruma /// Just for API sanity checking, they're already properly tested in the SDK/Ruma
class PermalinkTests: XCTestCase { @Suite
func testUserIdentifierPermalink() { struct PermalinkTests {
@Test
func userIdentifierPermalink() throws {
let invalidUserId = "This1sN0tV4lid!@#$%^&*()" let invalidUserId = "This1sN0tV4lid!@#$%^&*()"
XCTAssertNil(try? matrixToUserPermalink(userId: invalidUserId)) #expect(throws: (any Error).self) { try matrixToUserPermalink(userId: invalidUserId) }
let validUserId = "@abcdefghijklmnopqrstuvwxyz1234567890._-=/:matrix.org" 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" 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" url = "https://matrix.to/#/@bob:matrix.org?via=matrix.org"
XCTAssertEqual(parseMatrixEntityFrom(uri: url.absoluteString), #expect(parseMatrixEntityFrom(uri: url.absoluteString) ==
MatrixEntity(id: .user(id: "@bob:matrix.org"), MatrixEntity(id: .user(id: "@bob:matrix.org"),
via: ["matrix.org"])) via: ["matrix.org"]))
url = "https://matrix.to/#/!roomidentifier:matrix.org?via=matrix.org" url = "https://matrix.to/#/!roomidentifier:matrix.org?via=matrix.org"
XCTAssertEqual(parseMatrixEntityFrom(uri: url.absoluteString), #expect(parseMatrixEntityFrom(uri: url.absoluteString) ==
MatrixEntity(id: .room(id: "!roomidentifier:matrix.org"), MatrixEntity(id: .room(id: "!roomidentifier:matrix.org"),
via: ["matrix.org"])) via: ["matrix.org"]))
url = "https://matrix.to/#/%23roomalias:matrix.org?via=matrix.org" url = "https://matrix.to/#/%23roomalias:matrix.org?via=matrix.org"
XCTAssertEqual(parseMatrixEntityFrom(uri: url.absoluteString), #expect(parseMatrixEntityFrom(uri: url.absoluteString) ==
MatrixEntity(id: .roomAlias(alias: "#roomalias:matrix.org"), MatrixEntity(id: .roomAlias(alias: "#roomalias:matrix.org"),
via: ["matrix.org"])) via: ["matrix.org"]))
url = "https://matrix.to/#/!roomidentifier:matrix.org/$eventidentifier?via=matrix.org" url = "https://matrix.to/#/!roomidentifier:matrix.org/$eventidentifier?via=matrix.org"
XCTAssertEqual(parseMatrixEntityFrom(uri: url.absoluteString), #expect(parseMatrixEntityFrom(uri: url.absoluteString) ==
MatrixEntity(id: .eventOnRoomId(roomId: "!roomidentifier:matrix.org", eventId: "$eventidentifier"), MatrixEntity(id: .eventOnRoomId(roomId: "!roomidentifier:matrix.org", eventId: "$eventidentifier"),
via: ["matrix.org"])) via: ["matrix.org"]))
url = "https://matrix.to/#/#roomalias:matrix.org/$eventidentifier?via=matrix.org" url = "https://matrix.to/#/#roomalias:matrix.org/$eventidentifier?via=matrix.org"
XCTAssertEqual(parseMatrixEntityFrom(uri: url.absoluteString), #expect(parseMatrixEntityFrom(uri: url.absoluteString) ==
MatrixEntity(id: .eventOnRoomAlias(alias: "#roomalias:matrix.org", eventId: "$eventidentifier"), MatrixEntity(id: .eventOnRoomAlias(alias: "#roomalias:matrix.org", eventId: "$eventidentifier"),
via: ["matrix.org"])) via: ["matrix.org"]))
} }
} }

View File

@@ -8,11 +8,14 @@
import Combine import Combine
@testable import ElementX @testable import ElementX
import XCTest import Foundation
import Testing
@MainActor @MainActor
class PillContextTests: XCTestCase { @Suite
func testUser() async { struct PillContextTests {
@Test
func user() async {
let id = "@test:matrix.org" let id = "@test:matrix.org"
let proxyMock = JoinedRoomProxyMock(.init(name: "Test")) let proxyMock = JoinedRoomProxyMock(.init(name: "Test"))
let subject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([]) let subject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([])
@@ -30,19 +33,20 @@ class PillContextTests: XCTestCase {
timelineControllerFactory: TimelineControllerFactoryMock(.init())) timelineControllerFactory: TimelineControllerFactoryMock(.init()))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body))) let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body)))
XCTAssertFalse(context.viewState.isOwnMention) #expect(!context.viewState.isOwnMention)
XCTAssertEqual(context.viewState.displayText, id) #expect(context.viewState.displayText == id)
let name = "Mr. Test" let name = "Mr. Test"
let avatarURL = URL(string: "https://test.jpg") let avatarURL = URL(string: "https://test.jpg")
subject.send([RoomMemberProxyMock(with: .init(userID: id, displayName: name, avatarURL: avatarURL, membership: .join))]) subject.send([RoomMemberProxyMock(with: .init(userID: id, displayName: name, avatarURL: avatarURL, membership: .join))])
await Task.yield() await Task.yield()
XCTAssertFalse(context.viewState.isOwnMention) #expect(!context.viewState.isOwnMention)
XCTAssertEqual(context.viewState.displayText, "@\(name)") #expect(context.viewState.displayText == "@\(name)")
} }
func testOwnUser() { @Test
func ownUser() {
let id = "@test:matrix.org" let id = "@test:matrix.org"
let proxyMock = JoinedRoomProxyMock(.init(name: "Test", ownUserID: id)) let proxyMock = JoinedRoomProxyMock(.init(name: "Test", ownUserID: id))
let subject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([]) let subject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([])
@@ -60,10 +64,11 @@ class PillContextTests: XCTestCase {
timelineControllerFactory: TimelineControllerFactoryMock(.init())) timelineControllerFactory: TimelineControllerFactoryMock(.init()))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body))) 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 avatarURL = URL(string: "https://matrix.jpg")
let id = "test_room" let id = "test_room"
let displayName = "Test" let displayName = "Test"
@@ -83,11 +88,12 @@ class PillContextTests: XCTestCase {
timelineControllerFactory: TimelineControllerFactoryMock(.init())) timelineControllerFactory: TimelineControllerFactoryMock(.init()))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .allUsers, font: .preferredFont(forTextStyle: .body))) let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .allUsers, font: .preferredFont(forTextStyle: .body)))
XCTAssertTrue(context.viewState.isOwnMention) #expect(context.viewState.isOwnMention)
XCTAssertEqual(context.viewState.displayText, PillUtilities.atRoom) #expect(context.viewState.displayText == PillUtilities.atRoom)
} }
func testRoomIDMention() { @Test
func roomIDMention() {
let proxyMock = JoinedRoomProxyMock(.init()) let proxyMock = JoinedRoomProxyMock(.init())
let mockController = MockTimelineController() let mockController = MockTimelineController()
let clientMock = ClientProxyMock(.init()) let clientMock = ClientProxyMock(.init())
@@ -106,12 +112,13 @@ class PillContextTests: XCTestCase {
timelineControllerFactory: TimelineControllerFactoryMock(.init())) timelineControllerFactory: TimelineControllerFactoryMock(.init()))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .roomID("1"), font: .preferredFont(forTextStyle: .body))) let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .roomID("1"), font: .preferredFont(forTextStyle: .body)))
XCTAssertFalse(context.viewState.isOwnMention) #expect(!context.viewState.isOwnMention)
XCTAssertFalse(context.viewState.isUndefined) #expect(!context.viewState.isUndefined)
XCTAssertEqual(context.viewState.displayText, "#Foundation 🔭🪐🌌") #expect(context.viewState.displayText == "#Foundation 🔭🪐🌌")
} }
func testRoomIDMentionMissingRoom() { @Test
func roomIDMentionMissingRoom() {
let proxyMock = JoinedRoomProxyMock(.init()) let proxyMock = JoinedRoomProxyMock(.init())
let mockController = MockTimelineController() let mockController = MockTimelineController()
mockController.roomProxy = proxyMock mockController.roomProxy = proxyMock
@@ -128,12 +135,13 @@ class PillContextTests: XCTestCase {
timelineControllerFactory: TimelineControllerFactoryMock(.init())) timelineControllerFactory: TimelineControllerFactoryMock(.init()))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .roomID("1"), font: .preferredFont(forTextStyle: .body))) let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .roomID("1"), font: .preferredFont(forTextStyle: .body)))
XCTAssertFalse(context.viewState.isOwnMention) #expect(!context.viewState.isOwnMention)
XCTAssertFalse(context.viewState.isUndefined) #expect(!context.viewState.isUndefined)
XCTAssertEqual(context.viewState.displayText, "1") #expect(context.viewState.displayText == "1")
} }
func testRoomAliasMention() { @Test
func roomAliasMention() {
let proxyMock = JoinedRoomProxyMock(.init()) let proxyMock = JoinedRoomProxyMock(.init())
let mockController = MockTimelineController() let mockController = MockTimelineController()
mockController.roomProxy = proxyMock mockController.roomProxy = proxyMock
@@ -154,12 +162,13 @@ class PillContextTests: XCTestCase {
timelineControllerFactory: TimelineControllerFactoryMock(.init())) timelineControllerFactory: TimelineControllerFactoryMock(.init()))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .roomAlias("#foundation-and-empire:matrix.org"), font: .preferredFont(forTextStyle: .body))) let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .roomAlias("#foundation-and-empire:matrix.org"), font: .preferredFont(forTextStyle: .body)))
XCTAssertFalse(context.viewState.isOwnMention) #expect(!context.viewState.isOwnMention)
XCTAssertFalse(context.viewState.isUndefined) #expect(!context.viewState.isUndefined)
XCTAssertEqual(context.viewState.displayText, "#Foundation and Empire") #expect(context.viewState.displayText == "#Foundation and Empire")
} }
func testRoomAliasMentionMissingRoom() { @Test
func roomAliasMentionMissingRoom() {
let proxyMock = JoinedRoomProxyMock(.init()) let proxyMock = JoinedRoomProxyMock(.init())
let mockController = MockTimelineController() let mockController = MockTimelineController()
mockController.roomProxy = proxyMock mockController.roomProxy = proxyMock
@@ -176,12 +185,13 @@ class PillContextTests: XCTestCase {
timelineControllerFactory: TimelineControllerFactoryMock(.init())) timelineControllerFactory: TimelineControllerFactoryMock(.init()))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .roomAlias("#foundation-and-empire:matrix.org"), font: .preferredFont(forTextStyle: .body))) let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .roomAlias("#foundation-and-empire:matrix.org"), font: .preferredFont(forTextStyle: .body)))
XCTAssertFalse(context.viewState.isOwnMention) #expect(!context.viewState.isOwnMention)
XCTAssertFalse(context.viewState.isUndefined) #expect(!context.viewState.isUndefined)
XCTAssertEqual(context.viewState.displayText, "#foundation-and-empire:matrix.org") #expect(context.viewState.displayText == "#foundation-and-empire:matrix.org")
} }
func testEventOnRoomIDMention() { @Test
func eventOnRoomIDMention() {
let proxyMock = JoinedRoomProxyMock(.init()) let proxyMock = JoinedRoomProxyMock(.init())
let mockController = MockTimelineController() let mockController = MockTimelineController()
mockController.roomProxy = proxyMock mockController.roomProxy = proxyMock
@@ -200,12 +210,13 @@ class PillContextTests: XCTestCase {
timelineControllerFactory: TimelineControllerFactoryMock(.init())) timelineControllerFactory: TimelineControllerFactoryMock(.init()))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .event(room: .roomID("1")), font: .preferredFont(forTextStyle: .body))) let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .event(room: .roomID("1")), font: .preferredFont(forTextStyle: .body)))
XCTAssertFalse(context.viewState.isOwnMention) #expect(!context.viewState.isOwnMention)
XCTAssertFalse(context.viewState.isUndefined) #expect(!context.viewState.isUndefined)
XCTAssertEqual(context.viewState.displayText, "💬 > #Foundation 🔭🪐🌌") #expect(context.viewState.displayText == "💬 > #Foundation 🔭🪐🌌")
} }
func testEventOnRoomIDMentionMissingRoom() { @Test
func eventOnRoomIDMentionMissingRoom() {
let proxyMock = JoinedRoomProxyMock(.init()) let proxyMock = JoinedRoomProxyMock(.init())
let mockController = MockTimelineController() let mockController = MockTimelineController()
mockController.roomProxy = proxyMock mockController.roomProxy = proxyMock
@@ -222,12 +233,13 @@ class PillContextTests: XCTestCase {
timelineControllerFactory: TimelineControllerFactoryMock(.init())) timelineControllerFactory: TimelineControllerFactoryMock(.init()))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .event(room: .roomID("1")), font: .preferredFont(forTextStyle: .body))) let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .event(room: .roomID("1")), font: .preferredFont(forTextStyle: .body)))
XCTAssertFalse(context.viewState.isOwnMention) #expect(!context.viewState.isOwnMention)
XCTAssertFalse(context.viewState.isUndefined) #expect(!context.viewState.isUndefined)
XCTAssertEqual(context.viewState.displayText, "💬 > 1") #expect(context.viewState.displayText == "💬 > 1")
} }
func testEventOnRoomAliasMention() { @Test
func eventOnRoomAliasMention() {
let proxyMock = JoinedRoomProxyMock(.init()) let proxyMock = JoinedRoomProxyMock(.init())
let mockController = MockTimelineController() let mockController = MockTimelineController()
mockController.roomProxy = proxyMock mockController.roomProxy = proxyMock
@@ -248,12 +260,13 @@ class PillContextTests: XCTestCase {
timelineControllerFactory: TimelineControllerFactoryMock(.init())) timelineControllerFactory: TimelineControllerFactoryMock(.init()))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .event(room: .roomAlias("#foundation-and-empire:matrix.org")), font: .preferredFont(forTextStyle: .body))) 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) #expect(!context.viewState.isOwnMention)
XCTAssertFalse(context.viewState.isUndefined) #expect(!context.viewState.isUndefined)
XCTAssertEqual(context.viewState.displayText, "💬 > #Foundation and Empire") #expect(context.viewState.displayText == "💬 > #Foundation and Empire")
} }
func testEventOnRoomAliasMentionMissingRoom() { @Test
func eventOnRoomAliasMentionMissingRoom() {
let proxyMock = JoinedRoomProxyMock(.init()) let proxyMock = JoinedRoomProxyMock(.init())
let mockController = MockTimelineController() let mockController = MockTimelineController()
mockController.roomProxy = proxyMock mockController.roomProxy = proxyMock
@@ -270,8 +283,8 @@ class PillContextTests: XCTestCase {
timelineControllerFactory: TimelineControllerFactoryMock(.init())) timelineControllerFactory: TimelineControllerFactoryMock(.init()))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .event(room: .roomAlias("#foundation-and-empire:matrix.org")), font: .preferredFont(forTextStyle: .body))) 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) #expect(!context.viewState.isOwnMention)
XCTAssertFalse(context.viewState.isUndefined) #expect(!context.viewState.isUndefined)
XCTAssertEqual(context.viewState.displayText, "💬 > #foundation-and-empire:matrix.org") #expect(context.viewState.displayText == "💬 > #foundation-and-empire:matrix.org")
} }
} }

View File

@@ -7,119 +7,126 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class PinnedEventsBannerStateTests: XCTestCase { @Suite
func testEmpty() { struct PinnedEventsBannerStateTests {
@Test
func empty() {
var state = PinnedEventsBannerState.loading(numbersOfEvents: 0) var state = PinnedEventsBannerState.loading(numbersOfEvents: 0)
XCTAssertTrue(state.isEmpty) #expect(state.isEmpty)
state = .loaded(state: .init()) state = .loaded(state: .init())
XCTAssertTrue(state.isEmpty) #expect(state.isEmpty)
} }
func testLoading() { @Test
func loading() {
let originalState = PinnedEventsBannerState.loading(numbersOfEvents: 5) let originalState = PinnedEventsBannerState.loading(numbersOfEvents: 5)
var state = originalState var state = originalState
// This should not affect the state when loading // This should not affect the state when loading
state.previousPin() state.previousPin()
XCTAssertEqual(state, originalState) #expect(state == originalState)
XCTAssertTrue(state.isLoading) #expect(state.isLoading)
XCTAssertFalse(state.isEmpty) #expect(!state.isEmpty)
XCTAssertNil(state.selectedPinnedEventID) #expect(state.selectedPinnedEventID == nil)
XCTAssertEqual(state.displayedMessage.string, L10n.screenRoomPinnedBannerLoadingDescription) #expect(state.displayedMessage.string == L10n.screenRoomPinnedBannerLoadingDescription)
XCTAssertEqual(state.selectedPinnedIndex, 4) #expect(state.selectedPinnedIndex == 4)
XCTAssertEqual(state.count, 5) #expect(state.count == 5)
XCTAssertEqual(state.bannerIndicatorDescription.string, L10n.screenRoomPinnedBannerIndicatorDescription(L10n.screenRoomPinnedBannerIndicator(5, 5))) #expect(state.bannerIndicatorDescription.string == L10n.screenRoomPinnedBannerIndicatorDescription(L10n.screenRoomPinnedBannerIndicator(5, 5)))
} }
func testLoadingToLoaded() { @Test
func loadingToLoaded() {
var state = PinnedEventsBannerState.loading(numbersOfEvents: 2) var state = PinnedEventsBannerState.loading(numbersOfEvents: 2)
XCTAssertTrue(state.isLoading) #expect(state.isLoading)
state.setPinnedEventContents(["1": "test1", "2": "test2"]) state.setPinnedEventContents(["1": "test1", "2": "test2"])
XCTAssertEqual(state, .loaded(state: .init(pinnedEventContents: ["1": "test1", "2": "test2"], selectedPinnedEventID: "2"))) #expect(state == .loaded(state: .init(pinnedEventContents: ["1": "test1", "2": "test2"], selectedPinnedEventID: "2")))
XCTAssertFalse(state.isLoading) #expect(!state.isLoading)
} }
func testLoaded() { @Test
func loaded() {
let state = PinnedEventsBannerState.loaded(state: .init(pinnedEventContents: ["1": "test1", "2": "test2"], selectedPinnedEventID: "2")) let state = PinnedEventsBannerState.loaded(state: .init(pinnedEventContents: ["1": "test1", "2": "test2"], selectedPinnedEventID: "2"))
XCTAssertFalse(state.isLoading) #expect(!state.isLoading)
XCTAssertFalse(state.isEmpty) #expect(!state.isEmpty)
XCTAssertEqual(state.selectedPinnedEventID, "2") #expect(state.selectedPinnedEventID == "2")
XCTAssertEqual(state.displayedMessage.string, "test2") #expect(state.displayedMessage.string == "test2")
XCTAssertEqual(state.selectedPinnedIndex, 1) #expect(state.selectedPinnedIndex == 1)
XCTAssertEqual(state.count, 2) #expect(state.count == 2)
XCTAssertEqual(state.bannerIndicatorDescription.string, L10n.screenRoomPinnedBannerIndicatorDescription(L10n.screenRoomPinnedBannerIndicator(2, 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")) var state = PinnedEventsBannerState.loaded(state: .init(pinnedEventContents: ["1": "test1", "2": "test2", "3": "test3"], selectedPinnedEventID: "1"))
XCTAssertEqual(state.selectedPinnedEventID, "1") #expect(state.selectedPinnedEventID == "1")
XCTAssertEqual(state.selectedPinnedIndex, 0) #expect(state.selectedPinnedIndex == 0)
XCTAssertEqual(state.displayedMessage.string, "test1") #expect(state.displayedMessage.string == "test1")
state.previousPin() state.previousPin()
XCTAssertEqual(state.selectedPinnedEventID, "3") #expect(state.selectedPinnedEventID == "3")
XCTAssertEqual(state.selectedPinnedIndex, 2) #expect(state.selectedPinnedIndex == 2)
XCTAssertEqual(state.displayedMessage.string, "test3") #expect(state.displayedMessage.string == "test3")
state.previousPin() state.previousPin()
XCTAssertEqual(state.selectedPinnedEventID, "2") #expect(state.selectedPinnedEventID == "2")
XCTAssertEqual(state.selectedPinnedIndex, 1) #expect(state.selectedPinnedIndex == 1)
XCTAssertEqual(state.displayedMessage.string, "test2") #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")) var state = PinnedEventsBannerState.loaded(state: .init(pinnedEventContents: ["1": "test1", "2": "test2", "3": "test3", "4": "test4"], selectedPinnedEventID: "2"))
XCTAssertEqual(state.selectedPinnedEventID, "2") #expect(state.selectedPinnedEventID == "2")
XCTAssertEqual(state.selectedPinnedIndex, 1) #expect(state.selectedPinnedIndex == 1)
XCTAssertEqual(state.displayedMessage.string, "test2") #expect(state.displayedMessage.string == "test2")
XCTAssertEqual(state.count, 4) #expect(state.count == 4)
XCTAssertFalse(state.isEmpty) #expect(!state.isEmpty)
// let's remove the selected item // let's remove the selected item
state.setPinnedEventContents(["1": "test1", "3": "test3", "4": "test4"]) state.setPinnedEventContents(["1": "test1", "3": "test3", "4": "test4"])
// new selected item is the new latest // new selected item is the new latest
XCTAssertEqual(state.selectedPinnedEventID, "4") #expect(state.selectedPinnedEventID == "4")
XCTAssertEqual(state.selectedPinnedIndex, 2) #expect(state.selectedPinnedIndex == 2)
XCTAssertEqual(state.displayedMessage.string, "test4") #expect(state.displayedMessage.string == "test4")
XCTAssertEqual(state.count, 3) #expect(state.count == 3)
XCTAssertFalse(state.isEmpty) #expect(!state.isEmpty)
// let's add a new item at the top // let's add a new item at the top
state.setPinnedEventContents(["0": "test0", "1": "test1", "3": "test3", "4": "test4"]) state.setPinnedEventContents(["0": "test0", "1": "test1", "3": "test3", "4": "test4"])
// selected item doesn't change // selected item doesn't change
XCTAssertEqual(state.selectedPinnedEventID, "4") #expect(state.selectedPinnedEventID == "4")
// but the index is updated // but the index is updated
XCTAssertEqual(state.selectedPinnedIndex, 3) #expect(state.selectedPinnedIndex == 3)
XCTAssertEqual(state.displayedMessage.string, "test4") #expect(state.displayedMessage.string == "test4")
XCTAssertEqual(state.count, 4) #expect(state.count == 4)
XCTAssertFalse(state.isEmpty) #expect(!state.isEmpty)
// let's add a new item at the bottom // let's add a new item at the bottom
state.setPinnedEventContents(["0": "test0", "1": "test1", "3": "test3", "4": "test4", "5": "test5"]) state.setPinnedEventContents(["0": "test0", "1": "test1", "3": "test3", "4": "test4", "5": "test5"])
// selected item doesn't change // selected item doesn't change
XCTAssertEqual(state.selectedPinnedEventID, "4") #expect(state.selectedPinnedEventID == "4")
// and index stays the same // and index stays the same
XCTAssertEqual(state.selectedPinnedIndex, 3) #expect(state.selectedPinnedIndex == 3)
XCTAssertEqual(state.displayedMessage.string, "test4") #expect(state.displayedMessage.string == "test4")
XCTAssertEqual(state.count, 5) #expect(state.count == 5)
XCTAssertFalse(state.isEmpty) #expect(!state.isEmpty)
// set to tempty // set to tempty
state.setPinnedEventContents([:]) state.setPinnedEventContents([:])
XCTAssertTrue(state.isEmpty) #expect(state.isEmpty)
XCTAssertNil(state.selectedPinnedEventID) #expect(state.selectedPinnedEventID == nil)
// set to one item // set to one item
state.setPinnedEventContents(["6": "test6", "7": "test7"]) state.setPinnedEventContents(["6": "test6", "7": "test7"])
XCTAssertEqual(state.selectedPinnedEventID, "7") #expect(state.selectedPinnedEventID == "7")
XCTAssertEqual(state.selectedPinnedIndex, 1) #expect(state.selectedPinnedIndex == 1)
XCTAssertEqual(state.displayedMessage.string, "test7") #expect(state.displayedMessage.string == "test7")
XCTAssertEqual(state.count, 2) #expect(state.count == 2)
XCTAssertFalse(state.isEmpty) #expect(!state.isEmpty)
} }
} }

View File

@@ -7,165 +7,186 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class PollFormScreenViewModelTests: XCTestCase { @Suite
let timelineProxy = TimelineProxyMock(.init()) struct PollFormScreenViewModelTests {
private let timelineProxy = TimelineProxyMock(.init())
var viewModel: PollFormScreenViewModelProtocol! private var viewModel: PollFormScreenViewModelProtocol!
var context: PollFormScreenViewModelType.Context { private var context: PollFormScreenViewModelType.Context {
viewModel.context viewModel.context
} }
func testNewPollInitialState() async throws { @Test
mutating func newPollInitialState() async throws {
setupViewModel() setupViewModel()
#expect(context.options.count == 2)
XCTAssertEqual(context.options.count, 2) // This due to a bug in Swift testing that raises an error when allSatisfy is used in an #expect
XCTAssertTrue(context.options.allSatisfy(\.text.isEmpty)) let isEmpty = context.options.allSatisfy(\.text.isEmpty)
XCTAssertTrue(context.question.isEmpty) #expect(isEmpty)
XCTAssertTrue(context.viewState.isSubmitButtonDisabled) #expect(context.question.isEmpty)
XCTAssertFalse(context.viewState.bindings.isUndisclosed) #expect(context.viewState.isSubmitButtonDisabled)
#expect(!context.viewState.bindings.isUndisclosed)
// Cancellation should work without confirmation // Cancellation should work without confirmation
let deferred = deferFulfillment(viewModel.actions) { _ in true } let deferred = deferFulfillment(viewModel.actions) { _ in true }
context.send(viewAction: .cancel) context.send(viewAction: .cancel)
let action = try await deferred.fulfill() let action = try await deferred.fulfill()
XCTAssertNil(context.alertInfo) #expect(context.alertInfo == nil)
XCTAssertEqual(action, .close) #expect(action == .close)
} }
func testEditPollInitialState() async throws { @Test
mutating func editPollInitialState() async throws {
setupViewModel(mode: .edit(eventID: "foo", poll: .emptyDisclosed)) setupViewModel(mode: .edit(eventID: "foo", poll: .emptyDisclosed))
XCTAssertEqual(context.options.count, 3) #expect(context.options.count == 3)
XCTAssertTrue(context.options.allSatisfy { !$0.text.isEmpty }) #expect(context.options.allSatisfy { !$0.text.isEmpty })
XCTAssertFalse(context.question.isEmpty) #expect(!context.question.isEmpty)
XCTAssertTrue(context.viewState.isSubmitButtonDisabled) #expect(context.viewState.isSubmitButtonDisabled)
XCTAssertFalse(context.viewState.bindings.isUndisclosed) #expect(!context.viewState.bindings.isUndisclosed)
// Cancellation should work without confirmation // Cancellation should work without confirmation
let deferred = deferFulfillment(viewModel.actions) { _ in true } let deferred = deferFulfillment(viewModel.actions) { _ in true }
context.send(viewAction: .cancel) context.send(viewAction: .cancel)
let action = try await deferred.fulfill() let action = try await deferred.fulfill()
XCTAssertNil(context.alertInfo) #expect(context.alertInfo == nil)
XCTAssertEqual(action, .close) #expect(action == .close)
} }
func testNewPollInvalidEmptyOption() { @Test
mutating func newPollInvalidEmptyOption() {
setupViewModel() setupViewModel()
context.question = "foo" context.question = "foo"
context.options[0].text = "bla" context.options[0].text = "bla"
context.options[1].text = "bla" context.options[1].text = "bla"
context.send(viewAction: .addOption) 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)) setupViewModel(mode: .edit(eventID: "foo", poll: .emptyDisclosed))
context.send(viewAction: .addOption) context.send(viewAction: .addOption)
XCTAssertTrue(context.viewState.isSubmitButtonDisabled) #expect(context.viewState.isSubmitButtonDisabled)
// Cancellation requires a confirmation // Cancellation requires a confirmation
context.send(viewAction: .cancel) context.send(viewAction: .cancel)
XCTAssertNotNil(context.alertInfo) #expect(context.alertInfo != nil)
} }
func testEditPollSubmitButtonState() { @Test
mutating func editPollSubmitButtonState() {
setupViewModel(mode: .edit(eventID: "foo", poll: .emptyDisclosed)) setupViewModel(mode: .edit(eventID: "foo", poll: .emptyDisclosed))
XCTAssertTrue(context.viewState.isSubmitButtonDisabled) #expect(context.viewState.isSubmitButtonDisabled)
context.options[0].text = "foo" context.options[0].text = "foo"
XCTAssertFalse(context.viewState.isSubmitButtonDisabled) #expect(!context.viewState.isSubmitButtonDisabled)
// Cancellation requires a confirmation // Cancellation requires a confirmation
context.send(viewAction: .cancel) context.send(viewAction: .cancel)
XCTAssertNotNil(context.alertInfo) #expect(context.alertInfo != nil)
} }
func testNewPollSubmit() async throws { @Test
mutating func newPollSubmit() async throws {
setupViewModel() setupViewModel()
context.question = "foo" context.question = "foo"
context.options[0].text = "bla1" context.options[0].text = "bla1"
context.options[1].text = "bla2" context.options[1].text = "bla2"
XCTAssertFalse(context.viewState.isSubmitButtonDisabled) #expect(!context.viewState.isSubmitButtonDisabled)
let deferred = deferFulfillment(viewModel.actions) { $0 == .close } 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 confirmation { confirmation in
try await deferred.fulfill() 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)) setupViewModel(mode: .edit(eventID: "foo", poll: .emptyDisclosed))
context.question = "What is your favorite country?" context.question = "What is your favorite country?"
context.options.append(.init(text: "France 🇫🇷")) context.options.append(.init(text: "France 🇫🇷"))
XCTAssertFalse(context.viewState.isSubmitButtonDisabled) #expect(!context.viewState.isSubmitButtonDisabled)
let deferred = deferFulfillment(viewModel.actions) { $0 == .close } 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 confirmation { confirmation in
try await deferred.fulfill() 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)) setupViewModel(mode: .edit(eventID: "foo", poll: .emptyDisclosed))
context.question = "What is your favorite country?" context.question = "What is your favorite country?"
context.options.append(.init(text: "France 🇫🇷")) 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) context.send(viewAction: .delete)
try await deferredFailure.fulfill() 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 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 confirmation { confirmation in
try await deferred.fulfill() 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 // MARK: - Helpers
private func setupViewModel(mode: PollFormMode = .new) { private mutating func setupViewModel(mode: PollFormMode = .new) {
viewModel = PollFormScreenViewModel(mode: mode, viewModel = PollFormScreenViewModel(mode: mode,
timelineController: MockTimelineController(timelineProxy: timelineProxy), timelineController: MockTimelineController(timelineProxy: timelineProxy),
analytics: ServiceLocator.shared.analytics, analytics: ServiceLocator.shared.analytics,

View File

@@ -9,89 +9,97 @@
import Combine import Combine
@testable import ElementX @testable import ElementX
import MatrixRustSDKMocks import MatrixRustSDKMocks
import XCTest import Testing
@MainActor @MainActor
final class QRCodeLoginScreenViewModelTests: XCTestCase { @Suite
private var qrLoginProgressSubject: CurrentValueSubject<QRLoginProgress, AuthenticationServiceError>! struct QRCodeLoginScreenViewModelTests {
private var qrCodeLoginService: QRCodeLoginServiceMock! private enum Mode { case login, linkDesktop, linkMobile }
private var linkMobileProgressSubject: CurrentValueSubject<LinkNewDeviceService.LinkMobileProgress, QRCodeLoginError>! var qrLoginProgressSubject: CurrentValueSubject<QRLoginProgress, AuthenticationServiceError>!
private var linkDesktopProgressSubject: CurrentValueSubject<LinkNewDeviceService.LinkDesktopProgress, QRCodeLoginError>! var qrCodeLoginService: QRCodeLoginServiceMock!
private var linkNewDeviceService: LinkNewDeviceServiceMock!
private var appMediator: AppMediatorMock! var linkMobileProgressSubject: CurrentValueSubject<LinkNewDeviceService.LinkMobileProgress, QRCodeLoginError>!
var linkDesktopProgressSubject: CurrentValueSubject<LinkNewDeviceService.LinkDesktopProgress, QRCodeLoginError>!
var linkNewDeviceService: LinkNewDeviceServiceMock!
private var viewModel: QRCodeLoginScreenViewModelProtocol! var appMediator: AppMediatorMock!
private var context: QRCodeLoginScreenViewModelType.Context {
var viewModel: QRCodeLoginScreenViewModelProtocol!
var context: QRCodeLoginScreenViewModelType.Context {
viewModel.context viewModel.context
} }
func testLoginInitialState() { @Test
setupViewModel(mode: .login) mutating func loginInitialState() {
setup(mode: .login)
XCTAssertEqual(context.viewState.state, .loginInstructions) #expect(context.viewState.state == .loginInstructions)
XCTAssertNil(context.qrResult) #expect(context.qrResult == nil)
XCTAssertFalse(qrCodeLoginService.loginWithQRCodeDataCalled) #expect(!qrCodeLoginService.loginWithQRCodeDataCalled)
XCTAssertFalse(appMediator.requestAuthorizationIfNeededCalled) #expect(!appMediator.requestAuthorizationIfNeededCalled)
XCTAssertFalse(appMediator.openAppSettingsCalled) #expect(!appMediator.openAppSettingsCalled)
XCTAssertFalse(linkNewDeviceService.linkMobileDeviceCalled) #expect(!linkNewDeviceService.linkMobileDeviceCalled)
XCTAssertFalse(linkNewDeviceService.linkDesktopDeviceWithCalled) #expect(!linkNewDeviceService.linkDesktopDeviceWithCalled)
} }
func testLinkDesktopInitialState() { @Test
setupViewModel(mode: .linkDesktop) mutating func linkDesktopInitialState() {
setup(mode: .linkDesktop)
XCTAssertEqual(context.viewState.state, .linkDesktopInstructions) #expect(context.viewState.state == .linkDesktopInstructions)
XCTAssertNil(context.qrResult) #expect(context.qrResult == nil)
XCTAssertFalse(linkNewDeviceService.linkDesktopDeviceWithCalled) #expect(!linkNewDeviceService.linkDesktopDeviceWithCalled)
XCTAssertFalse(appMediator.requestAuthorizationIfNeededCalled) #expect(!appMediator.requestAuthorizationIfNeededCalled)
XCTAssertFalse(appMediator.openAppSettingsCalled) #expect(!appMediator.openAppSettingsCalled)
XCTAssertFalse(linkNewDeviceService.linkMobileDeviceCalled) #expect(!linkNewDeviceService.linkMobileDeviceCalled)
XCTAssertFalse(qrCodeLoginService.loginWithQRCodeDataCalled) #expect(!qrCodeLoginService.loginWithQRCodeDataCalled)
} }
func testLinkMobileInitialState() { @Test
setupViewModel(mode: .linkMobile) mutating func linkMobileInitialState() {
setup(mode: .linkMobile)
XCTAssertTrue(context.viewState.state.isDisplayQR) #expect(context.viewState.state.isDisplayQR)
XCTAssertTrue(linkNewDeviceService.linkMobileDeviceCalled) #expect(linkNewDeviceService.linkMobileDeviceCalled)
XCTAssertFalse(linkNewDeviceService.linkDesktopDeviceWithCalled) #expect(!linkNewDeviceService.linkDesktopDeviceWithCalled)
XCTAssertFalse(qrCodeLoginService.loginWithQRCodeDataCalled) #expect(!qrCodeLoginService.loginWithQRCodeDataCalled)
XCTAssertNil(context.qrResult) #expect(context.qrResult == nil)
} }
func testRequestCameraPermission() async throws { @Test
setupViewModel(mode: .login) mutating func requestCameraPermission() async throws {
setup(mode: .login)
appMediator.requestAuthorizationIfNeededReturnValue = false appMediator.requestAuthorizationIfNeededReturnValue = false
XCTAssert(context.viewState.state == .loginInstructions) #expect(context.viewState.state == .loginInstructions)
let deferred = deferFulfillment(viewModel.context.$viewState) { state in let deferred = deferFulfillment(viewModel.context.$viewState) { state in
state.state == .error(.noCameraPermission) state.state == .error(.noCameraPermission)
} }
context.send(viewAction: .startScan) context.send(viewAction: .startScan)
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertTrue(appMediator.requestAuthorizationIfNeededCalled) #expect(appMediator.requestAuthorizationIfNeededCalled)
context.send(viewAction: .errorAction(.openSettings)) context.send(viewAction: .errorAction(.openSettings))
await Task.yield() await Task.yield()
XCTAssertTrue(appMediator.openAppSettingsCalled) #expect(appMediator.openAppSettingsCalled)
XCTAssertNil(context.qrResult) #expect(context.qrResult == nil)
} }
func testLogin() async throws { @Test
setupViewModel(mode: .login) mutating func login() async throws {
XCTAssert(context.viewState.state == .loginInstructions) setup(mode: .login)
#expect(context.viewState.state == .loginInstructions)
var deferred = deferFulfillment(context.$viewState) { state in var deferred = deferFulfillment(context.$viewState) { state in
state.state == .scan(.scanning) state.state == .scan(.scanning)
} }
context.send(viewAction: .startScan) context.send(viewAction: .startScan)
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertTrue(appMediator.requestAuthorizationIfNeededCalled) #expect(appMediator.requestAuthorizationIfNeededCalled)
deferred = deferFulfillment(context.$viewState) { state in deferred = deferFulfillment(context.$viewState) { state in
state.state == .scan(.connecting) state.state == .scan(.connecting)
@@ -121,14 +129,15 @@ final class QRCodeLoginScreenViewModelTests: XCTestCase {
try await deferredAction.fulfill() try await deferredAction.fulfill()
} }
func testLinkDesktopComputer() async throws { @Test
setupViewModel(mode: .linkDesktop) mutating func linkDesktopComputer() async throws {
XCTAssert(context.viewState.state == .linkDesktopInstructions) setup(mode: .linkDesktop)
#expect(context.viewState.state == .linkDesktopInstructions)
var deferred = deferFulfillment(context.$viewState) { $0.state == .scan(.scanning) } var deferred = deferFulfillment(context.$viewState) { $0.state == .scan(.scanning) }
context.send(viewAction: .startScan) context.send(viewAction: .startScan)
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertTrue(appMediator.requestAuthorizationIfNeededCalled) #expect(appMediator.requestAuthorizationIfNeededCalled)
deferred = deferFulfillment(context.$viewState) { $0.state == .scan(.connecting) } deferred = deferFulfillment(context.$viewState) { $0.state == .scan(.connecting) }
context.qrResult = .init() context.qrResult = .init()
@@ -146,7 +155,7 @@ final class QRCodeLoginScreenViewModelTests: XCTestCase {
try await deferredAction.fulfill() try await deferredAction.fulfill()
let currentState = context.viewState.state 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) linkDesktopProgressSubject.send(.syncingSecrets)
try await deferredFailure.fulfill() try await deferredFailure.fulfill()
@@ -158,9 +167,10 @@ final class QRCodeLoginScreenViewModelTests: XCTestCase {
try await deferredAction.fulfill() try await deferredAction.fulfill()
} }
func testLinkMobileDevice() async throws { @Test
setupViewModel(mode: .linkMobile) mutating func linkMobileDevice() async throws {
XCTAssert(context.viewState.state.isDisplayQR) setup(mode: .linkMobile)
#expect(context.viewState.state.isDisplayQR)
let checkCodeSender = CheckCodeSenderSDKMock() let checkCodeSender = CheckCodeSenderSDKMock()
let checkCodeSenderProxy = CheckCodeSenderProxy(underlyingSender: checkCodeSender) let checkCodeSenderProxy = CheckCodeSenderProxy(underlyingSender: checkCodeSender)
@@ -189,16 +199,14 @@ final class QRCodeLoginScreenViewModelTests: XCTestCase {
try await deferredAction.fulfill() try await deferredAction.fulfill()
let currentState = context.viewState.state 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) linkMobileProgressSubject.send(.done)
try await deferredFailure.fulfill() try await deferredFailure.fulfill()
} }
// MARK: - Helpers // MARK: - Helpers
enum Mode { case login, linkDesktop, linkMobile } private mutating func setup(mode: Mode) {
private func setupViewModel(mode: Mode) {
qrLoginProgressSubject = .init(.starting) qrLoginProgressSubject = .init(.starting)
qrCodeLoginService = QRCodeLoginServiceMock() qrCodeLoginService = QRCodeLoginServiceMock()
qrCodeLoginService.loginWithQRCodeDataReturnValue = qrLoginProgressSubject.asCurrentValuePublisher() qrCodeLoginService.loginWithQRCodeDataReturnValue = qrLoginProgressSubject.asCurrentValuePublisher()

View File

@@ -7,42 +7,45 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
class RemotePreferenceTests: XCTestCase { @Suite
func testOverrideAndReset() { struct RemotePreferenceTests {
@Test
func overrideAndReset() {
let preference = RemotePreference(0) let preference = RemotePreference(0)
XCTAssertEqual(preference.publisher.value, 0) #expect(preference.publisher.value == 0)
XCTAssertFalse(preference.isRemotelyConfigured) #expect(!preference.isRemotelyConfigured)
preference.applyRemoteValue(1) preference.applyRemoteValue(1)
XCTAssertEqual(preference.publisher.value, 1) #expect(preference.publisher.value == 1)
XCTAssertTrue(preference.isRemotelyConfigured) #expect(preference.isRemotelyConfigured)
preference.applyRemoteValue(2) preference.applyRemoteValue(2)
XCTAssertEqual(preference.publisher.value, 2) #expect(preference.publisher.value == 2)
XCTAssertTrue(preference.isRemotelyConfigured) #expect(preference.isRemotelyConfigured)
preference.reset() preference.reset()
XCTAssertEqual(preference.publisher.value, 0) #expect(preference.publisher.value == 0)
XCTAssertFalse(preference.isRemotelyConfigured) #expect(!preference.isRemotelyConfigured)
} }
func testOptionalOverride() { @Test
func optionalOverride() {
let preference: RemotePreference<String?> = .init("Hello") let preference: RemotePreference<String?> = .init("Hello")
XCTAssertEqual(preference.publisher.value, "Hello") #expect(preference.publisher.value == "Hello")
XCTAssertFalse(preference.isRemotelyConfigured) #expect(!preference.isRemotelyConfigured)
preference.applyRemoteValue("World") preference.applyRemoteValue("World")
XCTAssertEqual(preference.publisher.value, "World") #expect(preference.publisher.value == "World")
XCTAssertTrue(preference.isRemotelyConfigured) #expect(preference.isRemotelyConfigured)
preference.applyRemoteValue(nil) preference.applyRemoteValue(nil)
XCTAssertEqual(preference.publisher.value, nil) #expect(preference.publisher.value == nil)
XCTAssertTrue(preference.isRemotelyConfigured) #expect(preference.isRemotelyConfigured)
preference.reset() preference.reset()
XCTAssertEqual(preference.publisher.value, "Hello") #expect(preference.publisher.value == "Hello")
XCTAssertFalse(preference.isRemotelyConfigured) #expect(!preference.isRemotelyConfigured)
} }
} }

View File

@@ -7,15 +7,17 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class ReportContentScreenViewModelTests: XCTestCase { @Suite
struct ReportContentScreenViewModelTests {
let eventID = "test-id" let eventID = "test-id"
let senderID = "@meany:server.com" let senderID = "@meany:server.com"
let reportReason = "I don't like it." let reportReason = "I don't like it."
func testReportContent() async throws { @Test
func reportContent() async throws {
// Given the report content view for some content. // Given the report content view for some content.
let roomProxy = JoinedRoomProxyMock(.init(name: "test")) let roomProxy = JoinedRoomProxyMock(.init(name: "test"))
roomProxy.reportContentReasonReturnValue = .success(()) roomProxy.reportContentReasonReturnValue = .success(())
@@ -37,14 +39,15 @@ class ReportContentScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
// Then the content should be reported, but the user should not be included. // Then the content should be reported, but the user should not be included.
XCTAssertEqual(roomProxy.reportContentReasonCallsCount, 1, "The content should always be reported.") #expect(roomProxy.reportContentReasonCallsCount == 1, "The content should always be reported.")
XCTAssertEqual(roomProxy.reportContentReasonReceivedArguments?.eventID, eventID, "The event ID should match the content being reported.") #expect(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.") #expect(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.") #expect(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(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. // Given the report content view for some content.
let roomProxy = JoinedRoomProxyMock(.init(name: "test")) let roomProxy = JoinedRoomProxyMock(.init(name: "test"))
roomProxy.reportContentReasonReturnValue = .success(()) roomProxy.reportContentReasonReturnValue = .success(())
@@ -67,10 +70,10 @@ class ReportContentScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
// Then the content should be reported, and the user should be ignored. // Then the content should be reported, and the user should be ignored.
XCTAssertEqual(roomProxy.reportContentReasonCallsCount, 1, "The content should always be reported.") #expect(roomProxy.reportContentReasonCallsCount == 1, "The content should always be reported.")
XCTAssertEqual(roomProxy.reportContentReasonReceivedArguments?.eventID, eventID, "The event ID should match the content being reported.") #expect(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.") #expect(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.") #expect(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(clientProxy.ignoreUserReceivedUserID == senderID, "The ignored user ID should match the sender.")
} }
} }

View File

@@ -7,96 +7,108 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class ReportRoomScreenViewModelTests: XCTestCase { @Suite
var viewModel: ReportRoomScreenViewModelProtocol! struct ReportRoomScreenViewModelTests {
var roomProxy: JoinedRoomProxyMock! private var viewModel: ReportRoomScreenViewModelProtocol
private var roomProxy: JoinedRoomProxyMock
var context: ReportRoomScreenViewModelType.Context { private var context: ReportRoomScreenViewModelType.Context {
viewModel.context viewModel.context
} }
override func setUp() { init() {
roomProxy = JoinedRoomProxyMock(.init()) roomProxy = JoinedRoomProxyMock(.init())
viewModel = ReportRoomScreenViewModel(roomProxy: roomProxy, userIndicatorController: UserIndicatorControllerMock()) viewModel = ReportRoomScreenViewModel(roomProxy: roomProxy, userIndicatorController: UserIndicatorControllerMock())
} }
func testInitialState() { @Test
XCTAssertTrue(context.viewState.bindings.reason.isEmpty) func initialState() {
XCTAssertFalse(context.viewState.bindings.shouldLeaveRoom) #expect(context.viewState.bindings.reason.isEmpty)
#expect(!context.viewState.bindings.shouldLeaveRoom)
} }
func testReportSuccess() async throws { @Test
func reportSuccess() async throws {
let reason = "Spam" let reason = "Spam"
let expectation = XCTestExpectation(description: "Report success")
roomProxy.reportRoomReasonClosure = { reasonArgument in try await confirmation { confirmation in
defer { expectation.fulfill() } roomProxy.reportRoomReasonClosure = { reasonArgument in
XCTAssertEqual(reasonArgument, reason) #expect(reasonArgument == reason)
return .success(()) 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 reason = "Spam"
let reportExpectation = XCTestExpectation(description: "Report success")
roomProxy.reportRoomReasonClosure = { reasonArgument in try await confirmation(expectedCount: 2) { confirmation in
defer { reportExpectation.fulfill() } roomProxy.reportRoomReasonClosure = { reasonArgument in
XCTAssertEqual(reasonArgument, reason) #expect(reasonArgument == reason)
return .success(()) 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") #expect(roomProxy.reportRoomReasonCalled)
roomProxy.leaveRoomClosure = { #expect(roomProxy.leaveRoomCalled)
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()
} }
func testReportSuccessLeaveFails() async throws { @Test
func reportSuccessLeaveFails() async throws {
let reason = "Spam" let reason = "Spam"
let reportExpectation = XCTestExpectation(description: "Report success")
roomProxy.reportRoomReasonClosure = { reasonArgument in try await confirmation(expectedCount: 2) { confirmation in
defer { reportExpectation.fulfill() } roomProxy.reportRoomReasonClosure = { reasonArgument in
XCTAssertEqual(reasonArgument, reason) #expect(reasonArgument == reason)
return .success(()) 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") #expect(roomProxy.reportRoomReasonCalled)
roomProxy.leaveRoomClosure = { #expect(roomProxy.leaveRoomCalled)
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()
} }
} }

View File

@@ -7,47 +7,48 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class ResolveVerifiedUserSendFailureScreenViewModelTests: XCTestCase { @Suite
let roomProxy = JoinedRoomProxyMock(.init()) struct ResolveVerifiedUserSendFailureScreenViewModelTests {
var viewModel: ResolveVerifiedUserSendFailureScreenViewModel! private let roomProxy = JoinedRoomProxyMock(.init())
var context: ResolveVerifiedUserSendFailureScreenViewModel.Context {
viewModel.context
}
func testUnsignedDevice() async throws { @Test
func unsignedDevice() async throws {
// Given a failure where a single user has an unverified device // Given a failure where a single user has an unverified device
let userID = "@alice:matrix.org" 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. // Given a failure where a multiple users have unverified devices.
let userIDs = ["@alice:matrix.org", "@bob:matrix.org", "@charlie:matrix.org"] let userIDs = ["@alice:matrix.org", "@bob:matrix.org", "@charlie:matrix.org"]
let devices = Dictionary(uniqueKeysWithValues: userIDs.map { ($0, ["DEVICE1, DEVICE2"]) }) 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. // Given a failure where a single user's identity has changed.
let userID = "@alice:matrix.org" 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. // Given a failure where a multiple users have unverified devices.
let userIDs = ["@alice:matrix.org", "@bob:matrix.org", "@charlie:matrix.org"] 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 // MARK: Helpers
@@ -59,17 +60,18 @@ class ResolveVerifiedUserSendFailureScreenViewModelTests: XCTestCase {
userIndicatorController: UserIndicatorControllerMock()) 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 var remainingUserIDs = userIDs
let context = viewModel.context
while remainingUserIDs.count > 1 { while remainingUserIDs.count > 1 {
// Verify that the strings are being updated. // Verify that the strings are being updated.
if assertStrings { if assertStrings {
verifyDisplayName(from: remainingUserIDs) try verifyDisplayName(context: context, from: remainingUserIDs)
} }
// When resolving the first failure. // 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) context.send(viewAction: .resolveAndResend)
// Then the sheet should remain open for the next failure. // Then the sheet should remain open for the next failure.
@@ -80,7 +82,7 @@ class ResolveVerifiedUserSendFailureScreenViewModelTests: XCTestCase {
// Verify the final string. // Verify the final string.
if assertStrings { if assertStrings {
verifyDisplayName(from: remainingUserIDs) try verifyDisplayName(context: context, from: remainingUserIDs)
} }
// When resolving the final failure. // When resolving the final failure.
@@ -91,18 +93,12 @@ class ResolveVerifiedUserSendFailureScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
} }
private func verifyDisplayName(from remainingUserIDs: [String]) { private func verifyDisplayName(context: ResolveVerifiedUserSendFailureScreenViewModel.Context, from remainingUserIDs: [String]) throws {
guard let userID = remainingUserIDs.first else { let userID = try #require(remainingUserIDs.first, "There should be a user ID to check.")
XCTFail("There should be a user ID to check.") let displayName = try #require(roomProxy.membersPublisher.value.first { $0.userID == userID }?.displayName,
return "There should be a matching mock user")
}
guard let displayName = roomProxy.membersPublisher.value.first(where: { $0.userID == userID })?.displayName else { #expect(context.viewState.title.contains(displayName))
XCTFail("There should be a matching mock user") #expect(context.viewState.subtitle.contains(displayName))
return
}
XCTAssertTrue(context.viewState.title.contains(displayName))
XCTAssertTrue(context.viewState.subtitle.contains(displayName))
} }
} }

View File

@@ -7,11 +7,14 @@
// //
@testable import ElementX @testable import ElementX
import Foundation
import MatrixRustSDK import MatrixRustSDK
import XCTest import Testing
class RestorationTokenTests: XCTestCase { @Suite
func testDecodeTokenWithSlidingSyncProxy() throws { struct RestorationTokenTests {
@Test
func decodeTokenWithSlidingSyncProxy() throws {
// Given an encoded restoration token that contains a session with a sliding sync proxy. // Given an encoded restoration token that contains a session with a sliding sync proxy.
let originalToken = RestorationTokenV4(session: SessionV1(accessToken: "1234", let originalToken = RestorationTokenV4(session: SessionV1(accessToken: "1234",
refreshToken: "5678", refreshToken: "5678",
@@ -26,18 +29,14 @@ class RestorationTokenTests: XCTestCase {
let data = try JSONEncoder().encode(originalToken) let data = try JSONEncoder().encode(originalToken)
// When decoding the data to the current restoration token format. // 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.
// Then an error should be thrown as it is no longer supported. #expect(throws: RestorationTokenError.slidingSyncProxyNotSupported) {
switch error { try JSONDecoder().decode(RestorationToken.self, from: data)
case RestorationTokenError.slidingSyncProxyNotSupported:
break
default:
XCTFail("Unexpected error thrown: \(error)")
}
} }
} }
func testDecodeFromTokenV4() throws { @Test
func decodeFromTokenV4() throws {
// Given an encoded restoration token in the 4th format that contains a stored session directory. // Given an encoded restoration token in the 4th format that contains a stored session directory.
let sessionDirectoryName = UUID().uuidString let sessionDirectoryName = UUID().uuidString
let originalToken = RestorationTokenV4(session: SessionV1(accessToken: "1234", 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. // Then the output should be a valid token with the expected store directories.
assertEqual(session: decodedToken.session, originalSession: originalToken.session) assertEqual(session: decodedToken.session, originalSession: originalToken.session)
XCTAssertEqual(decodedToken.passphrase, originalToken.passphrase, "The passphrase should not be changed.") #expect(decodedToken.passphrase == originalToken.passphrase, "The passphrase should not be changed.")
XCTAssertEqual(decodedToken.pusherNotificationClientIdentifier, originalToken.pusherNotificationClientIdentifier, #expect(decodedToken.pusherNotificationClientIdentifier == originalToken.pusherNotificationClientIdentifier,
"The push notification client identifier should not be changed.") "The push notification client identifier should not be changed.")
XCTAssertEqual(decodedToken.sessionDirectories.dataDirectory, originalToken.sessionDirectory, #expect(decodedToken.sessionDirectories.dataDirectory == originalToken.sessionDirectory,
"The session directory should not be changed.") "The session directory should not be changed.")
XCTAssertEqual(decodedToken.sessionDirectories.cacheDirectory, .sessionCachesBaseDirectory.appending(component: sessionDirectoryName), #expect(decodedToken.sessionDirectories.cacheDirectory == .sessionCachesBaseDirectory.appending(component: sessionDirectoryName),
"The cache directory should be derived from the session directory but in the caches directory.") "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. // Given an encoded restoration token in the 5th format that contains separate directories for session data and caches.
let sessionDirectoryName = UUID().uuidString let sessionDirectoryName = UUID().uuidString
let originalToken = RestorationTokenV5(session: SessionV1(accessToken: "1234", let originalToken = RestorationTokenV5(session: SessionV1(accessToken: "1234",
@@ -87,16 +87,17 @@ class RestorationTokenTests: XCTestCase {
// Then the output should be a valid token. // Then the output should be a valid token.
assertEqual(session: decodedToken.session, originalSession: originalToken.session) assertEqual(session: decodedToken.session, originalSession: originalToken.session)
XCTAssertEqual(decodedToken.passphrase, originalToken.passphrase, "The passphrase should not be changed.") #expect(decodedToken.passphrase == originalToken.passphrase, "The passphrase should not be changed.")
XCTAssertEqual(decodedToken.pusherNotificationClientIdentifier, originalToken.pusherNotificationClientIdentifier, #expect(decodedToken.pusherNotificationClientIdentifier == originalToken.pusherNotificationClientIdentifier,
"The push notification client identifier should not be changed.") "The push notification client identifier should not be changed.")
XCTAssertEqual(decodedToken.sessionDirectories.dataDirectory, originalToken.sessionDirectory, #expect(decodedToken.sessionDirectories.dataDirectory == originalToken.sessionDirectory,
"The session directory should not be changed.") "The session directory should not be changed.")
XCTAssertEqual(decodedToken.sessionDirectories.cacheDirectory, originalToken.cacheDirectory, #expect(decodedToken.sessionDirectories.cacheDirectory == originalToken.cacheDirectory,
"The cache directory should not be changed.") "The cache directory should not be changed.")
} }
func testDecodeFromCurrentToken() throws { @Test
func decodeFromCurrentToken() throws {
// Given an encoded restoration token in the current format. // Given an encoded restoration token in the current format.
let originalToken = RestorationToken(session: Session(accessToken: "1234", let originalToken = RestorationToken(session: Session(accessToken: "1234",
refreshToken: "5678", refreshToken: "5678",
@@ -114,16 +115,16 @@ class RestorationTokenTests: XCTestCase {
let decodedToken = try JSONDecoder().decode(RestorationToken.self, from: data) let decodedToken = try JSONDecoder().decode(RestorationToken.self, from: data)
// Then the output should be a valid token. // 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) { func assertEqual(session: Session, originalSession: SessionV1) {
XCTAssertEqual(session.accessToken, originalSession.accessToken, "The access token should not be changed.") #expect(session.accessToken == originalSession.accessToken, "The access token should not be changed.")
XCTAssertEqual(session.refreshToken, originalSession.refreshToken, "The refresh token should not be changed.") #expect(session.refreshToken == originalSession.refreshToken, "The refresh token should not be changed.")
XCTAssertEqual(session.userId, originalSession.userId, "The user ID should not be changed.") #expect(session.userId == originalSession.userId, "The user ID should not be changed.")
XCTAssertEqual(session.deviceId, originalSession.deviceId, "The device ID should not be changed.") #expect(session.deviceId == originalSession.deviceId, "The device ID should not be changed.")
XCTAssertEqual(session.homeserverUrl, originalSession.homeserverUrl, "The homeserver URL should not be changed.") #expect(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.oidcData == originalSession.oidcData, "The OIDC data should not be changed.")
} }
} }

View File

@@ -7,10 +7,11 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class RoomChangePermissionsScreenViewModelTests: XCTestCase { @Suite
struct RoomChangePermissionsScreenViewModelTests {
var roomProxy: JoinedRoomProxyMock! var roomProxy: JoinedRoomProxyMock!
var viewModel: RoomChangePermissionsScreenViewModelProtocol! var viewModel: RoomChangePermissionsScreenViewModelProtocol!
@@ -18,67 +19,62 @@ class RoomChangePermissionsScreenViewModelTests: XCTestCase {
viewModel.context viewModel.context
} }
func testChangeSetting() { @Test
setUp(isSpace: false) mutating func changeSetting() throws {
setup(isSpace: false)
// Given a screen with no changes. // Given a screen with no changes.
guard let index = context.settings[.roomDetails]?.firstIndex(where: { $0.keyPath == \.roomAvatar }) else { let index = try #require(context.settings[.roomDetails]?.firstIndex { $0.keyPath == \.roomAvatar },
XCTFail("There should be a setting for the room avatar.") "There should be a setting for the room avatar.")
return #expect(context.settings[.roomDetails]?[index].roleValue == .moderator)
} #expect(!context.viewState.hasChanges)
XCTAssertEqual(context.settings[.roomDetails]?[index].roleValue, .moderator)
XCTAssertFalse(context.viewState.hasChanges)
// When updating a setting. // When updating a setting.
let setting = RoomPermissionsSetting(title: "", let setting = RoomPermissionsSetting(title: "",
value: RoomRole.user.powerLevelValue, value: RoomRole.user.powerLevelValue,
ownPowerLevel: RoomRole.creator.powerLevel, ownPowerLevel: RoomRole.creator.powerLevel,
keyPath: \.roomAvatar) keyPath: \.roomAvatar)
XCTAssertFalse(setting.isDisabled) #expect(!setting.isDisabled)
XCTAssertEqual(setting.availableValues.map(\.tag), RoomPermissionsSetting.allValues.map(\.tag)) #expect(setting.availableValues.map(\.tag) == RoomPermissionsSetting.allValues.map(\.tag))
context.settings[.roomDetails]?[index] = setting context.settings[.roomDetails]?[index] = setting
// Then the setting should update and the changes should be flagged. // Then the setting should update and the changes should be flagged.
XCTAssertEqual(context.settings[.roomDetails]?[index].roleValue, .user) #expect(context.settings[.roomDetails]?[index].roleValue == .user)
XCTAssertTrue(context.viewState.hasChanges) #expect(context.viewState.hasChanges)
} }
func testSettingsCantBeChanged() { @Test
setUp(isSpace: false, ownPowerLevel: .value(25)) mutating func settingsCantBeChanged() throws {
setup(isSpace: false, ownPowerLevel: .value(25))
// Given a screen with no changes. // Given a screen with no changes.
guard let index = context.settings[.roomDetails]?.firstIndex(where: { $0.keyPath == \.roomAvatar }) else { var index = try #require(context.settings[.roomDetails]?.firstIndex { $0.keyPath == \.roomAvatar },
XCTFail("There should be a setting for the room avatar.") "There should be a setting for the room avatar.")
return #expect(context.settings[.roomDetails]?[index].roleValue == .moderator)
} #expect(context.settings[.roomDetails]?[index].isDisabled == true)
XCTAssertEqual(context.settings[.roomDetails]?[index].roleValue, .moderator) #expect(context.settings[.roomDetails]?[index].availableValues.count == 1)
XCTAssertEqual(context.settings[.roomDetails]?[index].isDisabled, true) #expect(!context.viewState.hasChanges)
XCTAssertEqual(context.settings[.roomDetails]?[index].availableValues.count, 1)
XCTAssertFalse(context.viewState.hasChanges)
guard let index = context.settings[.messagesAndContent]?.firstIndex(where: { $0.keyPath == \.eventsDefault }) else { index = try #require(context.settings[.messagesAndContent]?.firstIndex { $0.keyPath == \.eventsDefault },
XCTFail("There should be a setting for the events.") "There should be a setting for the events.")
return #expect(context.settings[.messagesAndContent]?[index].roleValue == .user)
} #expect(context.settings[.messagesAndContent]?[index].isDisabled == false)
XCTAssertEqual(context.settings[.messagesAndContent]?[index].roleValue, .user) #expect(context.settings[.messagesAndContent]?[index].availableValues.count == 1)
XCTAssertEqual(context.settings[.messagesAndContent]?[index].isDisabled, false)
XCTAssertEqual(context.settings[.messagesAndContent]?[index].availableValues.count, 1)
} }
func testSave() async throws { @Test
setUp(isSpace: false) mutating func save() async throws {
setup(isSpace: false)
// Given a screen with changes. // Given a screen with changes.
guard let index = context.settings[.roomDetails]?.firstIndex(where: { $0.keyPath == \.roomAvatar }) else { let index = try #require(context.settings[.roomDetails]?.firstIndex { $0.keyPath == \.roomAvatar },
XCTFail("There should be a setting for the room avatar.") "There should be a setting for the room avatar.")
return
}
context.settings[.roomDetails]?[index] = RoomPermissionsSetting(title: "", context.settings[.roomDetails]?[index] = RoomPermissionsSetting(title: "",
value: RoomRole.user.powerLevelValue, value: RoomRole.user.powerLevelValue,
ownPowerLevel: RoomRole.creator.powerLevel, ownPowerLevel: RoomRole.creator.powerLevel,
keyPath: \.roomAvatar) keyPath: \.roomAvatar)
XCTAssertEqual(context.settings[.roomDetails]?[index].roleValue, .user) #expect(context.settings[.roomDetails]?[index].roleValue == .user)
XCTAssertEqual(context.settings[.roomDetails]?[index].isDisabled, false) #expect(context.settings[.roomDetails]?[index].isDisabled == false)
XCTAssertEqual(context.settings[.roomDetails]?[index].availableValues.map(\.tag), RoomPermissionsSetting.allValues.map(\.tag)) #expect(context.settings[.roomDetails]?[index].availableValues.map(\.tag) == RoomPermissionsSetting.allValues.map(\.tag))
XCTAssertTrue(context.viewState.hasChanges) #expect(context.viewState.hasChanges)
XCTAssertEqual(context.settings.count, 3) #expect(context.settings.count == 3)
// When saving changes. // When saving changes.
context.send(viewAction: .save) context.send(viewAction: .save)
@@ -86,40 +82,45 @@ class RoomChangePermissionsScreenViewModelTests: XCTestCase {
try await Task.sleep(for: .milliseconds(100)) try await Task.sleep(for: .milliseconds(100))
// Then the changes should be applied. // Then the changes should be applied.
XCTAssertTrue(roomProxy.applyPowerLevelChangesCalled) #expect(roomProxy.applyPowerLevelChangesCalled)
XCTAssertEqual(roomProxy.applyPowerLevelChangesReceivedChanges, .init(roomAvatar: 0), #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.") "Only the avatar setting should be applied. No other settings were changed so they should be nil to remain left alone.")
} }
func testSaveNoChanges() { @Test
setUp(isSpace: false) mutating func saveNoChanges() {
setup(isSpace: false)
// Given a screen with no changes. // Given a screen with no changes.
XCTAssertFalse(context.viewState.hasChanges) #expect(!context.viewState.hasChanges)
// When saving changes. // When saving changes.
context.send(viewAction: .save) context.send(viewAction: .save)
// Then nothing should happen. // Then nothing should happen.
XCTAssertFalse(roomProxy.applyPowerLevelChangesCalled) #expect(!roomProxy.applyPowerLevelChangesCalled)
} }
func testDefaultStateRoom() { @Test
setUp(isSpace: false) mutating func defaultStateRoom() {
XCTAssertNotNil(context.settings[.roomDetails]) setup(isSpace: false)
XCTAssertNotNil(context.settings[.memberModeration]) #expect(context.settings[.roomDetails] != nil)
XCTAssertNotNil(context.settings[.messagesAndContent]) #expect(context.settings[.memberModeration] != nil)
XCTAssertNil(context.settings[.manageSpace]) #expect(context.settings[.messagesAndContent] != nil)
#expect(context.settings[.manageSpace] == nil)
} }
func testDefaultStateSpace() { @Test
setUp(isSpace: true) mutating func defaultStateSpace() {
XCTAssertNotNil(context.settings[.roomDetails]) setup(isSpace: true)
XCTAssertNotNil(context.settings[.memberModeration]) #expect(context.settings[.roomDetails] != nil)
XCTAssertNil(context.settings[.messagesAndContent]) #expect(context.settings[.memberModeration] != nil)
XCTAssertNotNil(context.settings[.manageSpace]) #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)) roomProxy = JoinedRoomProxyMock(.init(isSpace: isSpace))
viewModel = RoomChangePermissionsScreenViewModel(currentPermissions: .init(powerLevels: .mock), viewModel = RoomChangePermissionsScreenViewModel(currentPermissions: .init(powerLevels: .mock),
ownPowerLevel: ownPowerLevel, ownPowerLevel: ownPowerLevel,

View File

@@ -7,10 +7,11 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class RoomChangeRolesScreenViewModelTests: XCTestCase { @Suite
struct RoomChangeRolesScreenViewModelTests {
var viewModel: RoomChangeRolesScreenViewModelProtocol! var viewModel: RoomChangeRolesScreenViewModelProtocol!
var roomProxy: JoinedRoomProxyMock! var roomProxy: JoinedRoomProxyMock!
@@ -18,138 +19,136 @@ class RoomChangeRolesScreenViewModelTests: XCTestCase {
viewModel.context viewModel.context
} }
func testInitialStateAdministrators() { @Test
setupViewModel(mode: .administrator) mutating func initialStateAdministrators() {
XCTAssertEqual(context.viewState.membersToPromote, []) setup(mode: .administrator)
XCTAssertEqual(context.viewState.membersToDemote, []) #expect(context.viewState.membersToPromote == [])
XCTAssertEqual(context.viewState.administrators, context.viewState.visibleAdministrators) #expect(context.viewState.membersToDemote == [])
XCTAssertEqual(context.viewState.moderators, context.viewState.visibleModerators) #expect(context.viewState.administrators == context.viewState.visibleAdministrators)
XCTAssertEqual(context.viewState.users, context.viewState.visibleUsers) #expect(context.viewState.moderators == context.viewState.visibleModerators)
XCTAssertEqual(context.viewState.membersWithRole.count, 2) #expect(context.viewState.users == context.viewState.visibleUsers)
XCTAssertEqual(context.viewState.membersWithRole.first?.id, RoomMemberProxyMock.mockAdmin.userID) #expect(context.viewState.membersWithRole.count == 2)
XCTAssertFalse(context.viewState.hasChanges) #expect(context.viewState.membersWithRole.first?.id == RoomMemberProxyMock.mockAdmin.userID)
XCTAssertFalse(context.viewState.isSearching) #expect(!context.viewState.hasChanges)
#expect(!context.viewState.isSearching)
} }
func testInitialStateModerators() { @Test
setupViewModel(mode: .moderator) mutating func initialStateModerators() {
XCTAssertEqual(context.viewState.membersToPromote, []) setup(mode: .moderator)
XCTAssertEqual(context.viewState.membersToDemote, []) #expect(context.viewState.membersToPromote == [])
XCTAssertEqual(context.viewState.administrators, context.viewState.visibleAdministrators) #expect(context.viewState.membersToDemote == [])
XCTAssertEqual(context.viewState.moderators, context.viewState.visibleModerators) #expect(context.viewState.administrators == context.viewState.visibleAdministrators)
XCTAssertEqual(context.viewState.users, context.viewState.visibleUsers) #expect(context.viewState.moderators == context.viewState.visibleModerators)
XCTAssertEqual(context.viewState.membersWithRole.count, 3) #expect(context.viewState.users == context.viewState.visibleUsers)
XCTAssertNotNil(context.viewState.membersWithRole.first { $0.id == RoomMemberProxyMock.mockModerator.userID }) #expect(context.viewState.membersWithRole.count == 3)
XCTAssertFalse(context.viewState.hasChanges) #expect(context.viewState.membersWithRole.first { $0.id == RoomMemberProxyMock.mockModerator.userID } != nil)
XCTAssertFalse(context.viewState.isSearching) #expect(!context.viewState.hasChanges)
#expect(!context.viewState.isSearching)
} }
func testToggleUserOn() { @Test
testInitialStateModerators() mutating func toggleUserOn() throws {
guard let firstUser = context.viewState.users.first(where: { !context.viewState.isMemberSelected($0) }) else { setup(mode: .moderator)
XCTFail("There should be a regular user available to promote.") let firstUser = try #require(context.viewState.users.first { !context.viewState.isMemberSelected($0) },
return "There should be a regular user available to promote.")
}
context.send(viewAction: .toggleMember(firstUser)) context.send(viewAction: .toggleMember(firstUser))
XCTAssertEqual(context.viewState.membersToPromote, [firstUser]) #expect(context.viewState.membersToPromote == [firstUser])
XCTAssertEqual(context.viewState.membersToDemote, []) #expect(context.viewState.membersToDemote == [])
XCTAssertEqual(context.viewState.membersWithRole.count, 4) #expect(context.viewState.membersWithRole.count == 4)
XCTAssertTrue(context.viewState.membersWithRole.contains(firstUser)) #expect(context.viewState.membersWithRole.contains(firstUser))
XCTAssertTrue(context.viewState.hasChanges) #expect(context.viewState.hasChanges)
} }
func testToggleUserOff() { @Test
testToggleUserOn() mutating func toggleUserOff() throws {
guard let firstUser = context.viewState.membersToPromote.first else { try toggleUserOn()
XCTFail("There should be a promoted member before we begin.") let firstUser = try #require(context.viewState.membersToPromote.first,
return "There should be a regular user available to promote.")
}
// Then toggle off
context.send(viewAction: .toggleMember(firstUser)) context.send(viewAction: .toggleMember(firstUser))
XCTAssertEqual(context.viewState.membersToPromote, []) #expect(context.viewState.membersToPromote == [])
XCTAssertEqual(context.viewState.membersToDemote, []) #expect(context.viewState.membersToDemote == [])
XCTAssertEqual(context.viewState.membersWithRole.count, 3) #expect(context.viewState.membersWithRole.count == 3)
XCTAssertFalse(context.viewState.membersWithRole.contains(firstUser)) #expect(!context.viewState.membersWithRole.contains(firstUser))
XCTAssertFalse(context.viewState.hasChanges) #expect(!context.viewState.hasChanges)
} }
func testDemoteToggledUser() { @Test
testToggleUserOn() mutating func demoteToggledUser() throws {
guard let firstUser = context.viewState.membersToPromote.first else { try toggleUserOn()
XCTFail("There should be a promoted member before we begin.") let firstUser = try #require(context.viewState.membersToPromote.first,
return "There should be a regular user available to promote.")
}
// Then demote
context.send(viewAction: .demoteMember(firstUser)) context.send(viewAction: .demoteMember(firstUser))
XCTAssertEqual(context.viewState.membersToPromote, []) #expect(context.viewState.membersToPromote == [])
XCTAssertEqual(context.viewState.membersToDemote, []) #expect(context.viewState.membersToDemote == [])
XCTAssertEqual(context.viewState.membersWithRole.count, 3) #expect(context.viewState.membersWithRole.count == 3)
XCTAssertFalse(context.viewState.membersWithRole.contains(firstUser)) #expect(!context.viewState.membersWithRole.contains(firstUser))
XCTAssertFalse(context.viewState.hasChanges) #expect(!context.viewState.hasChanges)
} }
func testToggleModeratorOff() { @Test
testInitialStateModerators() mutating func toggleModeratorOff() throws {
guard let existingModerator = context.viewState.membersWithRole.first(where: { $0.role == .moderator }) else { initialStateModerators()
XCTFail("There should be a member with the role before we begin.") let existingModerator = try #require(context.viewState.membersWithRole.first { $0.role == .moderator },
return "There should be a member with the role before we begin.")
}
context.send(viewAction: .toggleMember(existingModerator)) context.send(viewAction: .toggleMember(existingModerator))
XCTAssertEqual(context.viewState.membersToPromote, []) #expect(context.viewState.membersToPromote == [])
XCTAssertEqual(context.viewState.membersToDemote, [existingModerator]) #expect(context.viewState.membersToDemote == [existingModerator])
XCTAssertEqual(context.viewState.membersWithRole.count, 2) #expect(context.viewState.membersWithRole.count == 2)
XCTAssertFalse(context.viewState.membersWithRole.contains(existingModerator)) #expect(!context.viewState.membersWithRole.contains(existingModerator))
XCTAssertTrue(context.viewState.hasChanges) #expect(context.viewState.hasChanges)
} }
func testToggleModeratorOn() { @Test
testToggleModeratorOff() mutating func toggleModeratorOn() throws {
try toggleModeratorOff()
guard let demotedMember = context.viewState.membersToDemote.first else { let demotedMember = try #require(context.viewState.membersToDemote.first,
XCTFail("There should be a member selected to demote before we begin.") "There should be a member with the role before we begin.")
return
}
// Then toggle back on
context.send(viewAction: .toggleMember(demotedMember)) context.send(viewAction: .toggleMember(demotedMember))
XCTAssertEqual(context.viewState.membersToPromote, []) #expect(context.viewState.membersToPromote == [])
XCTAssertEqual(context.viewState.membersToDemote, []) #expect(context.viewState.membersToDemote == [])
XCTAssertEqual(context.viewState.membersWithRole.count, 3) #expect(context.viewState.membersWithRole.count == 3)
XCTAssertTrue(context.viewState.membersWithRole.contains(demotedMember)) #expect(context.viewState.membersWithRole.contains(demotedMember))
XCTAssertFalse(context.viewState.hasChanges) #expect(!context.viewState.hasChanges)
} }
func testDemoteModerator() { @Test
testInitialStateModerators() mutating func demoteModerator() throws {
guard let existingModerator = context.viewState.membersWithRole.first(where: { $0.role == .moderator }) else { initialStateModerators()
XCTFail("There should be a member with the role before we begin.") let existingModerator = try #require(context.viewState.membersWithRole.first { $0.role == .moderator },
return "There should be a member with the role before we begin.")
}
context.send(viewAction: .demoteMember(existingModerator)) context.send(viewAction: .demoteMember(existingModerator))
XCTAssertEqual(context.viewState.membersToPromote, []) #expect(context.viewState.membersToPromote == [])
XCTAssertEqual(context.viewState.membersToDemote, [existingModerator]) #expect(context.viewState.membersToDemote == [existingModerator])
XCTAssertEqual(context.viewState.membersWithRole.count, 2) #expect(context.viewState.membersWithRole.count == 2)
XCTAssertFalse(context.viewState.membersWithRole.contains(existingModerator)) #expect(!context.viewState.membersWithRole.contains(existingModerator))
XCTAssertTrue(context.viewState.hasChanges) #expect(context.viewState.hasChanges)
} }
func testSaveModeratorChanges() async throws { @Test
mutating func saveModeratorChanges() async throws {
// Given the change roles view model for moderators. // 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 firstUser = try #require(context.viewState.users.first { !context.viewState.isMemberSelected($0) },
let existingModerator = context.viewState.membersWithRole.first(where: { $0.role == .moderator }) else { "There should be a regular user to begin with.")
XCTFail("There should be a regular user and a moderator to begin with.") let existingModerator = try #require(context.viewState.membersWithRole.first { $0.role == .moderator },
return "There should be a moderator to begin with.")
}
// When promoting a regular user and demoting a moderator. // When promoting a regular user and demoting a moderator.
context.send(viewAction: .toggleMember(firstUser)) context.send(viewAction: .toggleMember(firstUser))
@@ -159,40 +158,41 @@ class RoomChangeRolesScreenViewModelTests: XCTestCase {
try await Task.sleep(for: .milliseconds(100)) 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. // Then no warning should be shown, and the call to update the users should be made straight away.
XCTAssertTrue(roomProxy.updatePowerLevelsForUsersCalled) #expect(roomProxy.updatePowerLevelsForUsersCalled)
XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.count, 2) #expect(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.count == 2)
XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.contains { $0.userID == existingModerator.id && $0.powerLevel == 0 }, true) #expect(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.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. // Given the change roles view model for administrators.
setupViewModel(mode: .administrator) setup(mode: .administrator)
XCTAssertNil(context.alertInfo) #expect(context.alertInfo == nil)
guard let firstUser = context.viewState.users.first(where: { !context.viewState.isMemberSelected($0) }) else { let firstUser = try #require(context.viewState.users.first { !context.viewState.isMemberSelected($0) },
XCTFail("There should be a regular user to begin with.") "There should be a regular user to begin with.")
return
}
// When saving changes to promote a user to an administrator. // When saving changes to promote a user to an administrator.
context.send(viewAction: .toggleMember(firstUser)) context.send(viewAction: .toggleMember(firstUser))
context.send(viewAction: .save) context.send(viewAction: .save)
// Then an alert should be shown to warn the action cannot be undone. // Then an alert should be shown to warn the action cannot be undone.
XCTAssertNotNil(context.alertInfo) #expect(context.alertInfo != nil)
// When confirming the prompt // When confirming the prompt
context.alertInfo?.primaryButton.action?() context.alertInfo?.primaryButton.action?()
try await Task.sleep(for: .milliseconds(100)) try await Task.sleep(for: .milliseconds(100))
// Then the user should be made into an administrator. // Then the user should be made into an administrator.
XCTAssertTrue(roomProxy.updatePowerLevelsForUsersCalled) #expect(roomProxy.updatePowerLevelsForUsersCalled)
XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.count, 1) #expect(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.count == 1)
XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.contains { $0.userID == firstUser.id && $0.powerLevel == 100 }, true) #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)) roomProxy = JoinedRoomProxyMock(.init(members: .allMembersAsAdmin))
viewModel = RoomChangeRolesScreenViewModel(mode: mode, viewModel = RoomChangeRolesScreenViewModel(mode: mode,
roomProxy: roomProxy, roomProxy: roomProxy,

View File

@@ -1,13 +0,0 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
@testable import ElementX
import XCTest
@MainActor
class RoomDirectorySearchScreenScreenViewModelTests: XCTestCase { }

View File

@@ -8,13 +8,14 @@
@testable import ElementX @testable import ElementX
import MatrixRustSDK import MatrixRustSDK
import XCTest import Testing
class RoomEventStringBuilderTests: XCTestCase { @Suite
var ownUserID: String! struct RoomEventStringBuilderTests {
var stringBuilder: RoomEventStringBuilder! private let ownUserID: String
private let stringBuilder: RoomEventStringBuilder
override func setUp() { init() {
ownUserID = "@alice:matrix.org" ownUserID = "@alice:matrix.org"
let stateEventStringBuilder = RoomStateEventStringBuilder(userID: ownUserID) let stateEventStringBuilder = RoomStateEventStringBuilder(userID: ownUserID)
let attributedStringBuilder = AttributedStringBuilder(mentionBuilder: MentionBuilder()) let attributedStringBuilder = AttributedStringBuilder(mentionBuilder: MentionBuilder())
@@ -26,36 +27,37 @@ class RoomEventStringBuilderTests: XCTestCase {
shouldPrefixSenderName: true) shouldPrefixSenderName: true)
} }
func testSenderPrefix() { @Test
func senderPrefix() {
let ownMessageString = stringBuilder.buildAttributedString(for: makeMessageItem(senderID: ownUserID, senderDisplayName: "Alice")) 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")) 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", let ambiguousMessageString = stringBuilder.buildAttributedString(for: makeMessageItem(senderID: "@charlie:matrix.org",
senderDisplayName: "Charlie", senderDisplayName: "Charlie",
senderDisplayNameAmbiguous: true)) senderDisplayNameAmbiguous: true))
XCTAssertEqual(ambiguousMessageString?.string, "Charlie (@charlie:matrix.org): Hello, World!", #expect(ambiguousMessageString?.string == "Charlie (@charlie:matrix.org): Hello, World!",
"Messages from senders with ambiguous display names should include their user ID in the prefix.") "Messages from senders with ambiguous display names should include their user ID in the prefix.")
let ownEmoteString = stringBuilder.buildAttributedString(for: makeMessageItem(senderID: ownUserID, let ownEmoteString = stringBuilder.buildAttributedString(for: makeMessageItem(senderID: ownUserID,
senderDisplayName: "Alice", senderDisplayName: "Alice",
type: .emote, type: .emote,
message: "laughs")) 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", let otherEmoteString = stringBuilder.buildAttributedString(for: makeMessageItem(senderID: "@bob:matrix.org",
senderDisplayName: "Bob", senderDisplayName: "Bob",
type: .emote, type: .emote,
message: "sighs")) 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")) 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")) 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 // MARK: - Helpers

View File

@@ -7,116 +7,122 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
final class RoomListFiltersStateTests: XCTestCase { @Suite
var appSettings: AppSettings! final class RoomListFiltersStateTests {
var appSettings: AppSettings
var state: RoomListFiltersState
let allCasesWithoutLowPriority = RoomListFilter.allCases.filter { $0 != .lowPriority }
var state: RoomListFiltersState! init() {
var allCasesWithoutLowPriority = RoomListFilter.allCases.filter { $0 != .lowPriority }
override func setUp() {
AppSettings.resetAllSettings() AppSettings.resetAllSettings()
appSettings = AppSettings() appSettings = AppSettings()
state = RoomListFiltersState(appSettings: appSettings) state = RoomListFiltersState(appSettings: appSettings)
} }
override func tearDown() { deinit {
AppSettings.resetAllSettings() AppSettings.resetAllSettings()
} }
func testInitialState() { @Test
XCTAssertFalse(state.isFiltering) func initialState() {
XCTAssertEqual(state.activeFilters, []) #expect(!state.isFiltering)
XCTAssertEqual(state.availableFilters, allCasesWithoutLowPriority) #expect(state.activeFilters == [])
#expect(state.availableFilters == allCasesWithoutLowPriority)
} }
func testSetAndUnsetFilters() { @Test
func setAndUnsetFilters() {
state.activateFilter(.unreads) state.activateFilter(.unreads)
XCTAssertTrue(state.isFiltering) #expect(state.isFiltering)
XCTAssertEqual(state.activeFilters, [.unreads]) #expect(state.activeFilters == [.unreads])
XCTAssertEqual(state.availableFilters, [.people, .rooms, .favourites]) #expect(state.availableFilters == [.people, .rooms, .favourites])
state.deactivateFilter(.unreads) state.deactivateFilter(.unreads)
XCTAssertFalse(state.isFiltering) #expect(!state.isFiltering)
XCTAssertEqual(state.activeFilters, []) #expect(state.activeFilters == [])
XCTAssertEqual(state.availableFilters, allCasesWithoutLowPriority) #expect(state.availableFilters == allCasesWithoutLowPriority)
} }
func testMutuallyExclusiveFilters() { @Test
func mutuallyExclusiveFilters() {
state.activateFilter(.people) state.activateFilter(.people)
XCTAssertTrue(state.isFiltering) #expect(state.isFiltering)
XCTAssertEqual(state.activeFilters, [.people]) #expect(state.activeFilters == [.people])
XCTAssertEqual(state.availableFilters, [.unreads, .favourites]) #expect(state.availableFilters == [.unreads, .favourites])
state.deactivateFilter(.people) state.deactivateFilter(.people)
XCTAssertFalse(state.isFiltering) #expect(!state.isFiltering)
XCTAssertEqual(state.activeFilters, []) #expect(state.activeFilters == [])
XCTAssertEqual(state.availableFilters, allCasesWithoutLowPriority) #expect(state.availableFilters == allCasesWithoutLowPriority)
state.activateFilter(.rooms) state.activateFilter(.rooms)
XCTAssertTrue(state.isFiltering) #expect(state.isFiltering)
XCTAssertEqual(state.activeFilters, [.rooms]) #expect(state.activeFilters == [.rooms])
XCTAssertEqual(state.availableFilters, [.unreads, .favourites]) #expect(state.availableFilters == [.unreads, .favourites])
state.activateFilter(.unreads) state.activateFilter(.unreads)
XCTAssertTrue(state.isFiltering) #expect(state.isFiltering)
XCTAssertEqual(state.activeFilters, [.rooms, .unreads]) #expect(state.activeFilters == [.rooms, .unreads])
XCTAssertEqual(state.availableFilters, [.favourites]) #expect(state.availableFilters == [.favourites])
} }
func testClearFilters() { @Test
func clearFilters() {
state.activateFilter(.people) state.activateFilter(.people)
XCTAssertEqual(state.activeFilters, [.people]) #expect(state.activeFilters == [.people])
XCTAssertEqual(state.availableFilters, [.unreads, .favourites]) #expect(state.availableFilters == [.unreads, .favourites])
state.activateFilter(.unreads) state.activateFilter(.unreads)
XCTAssertEqual(state.activeFilters, [.people, .unreads]) #expect(state.activeFilters == [.people, .unreads])
XCTAssertEqual(state.availableFilters, [.favourites]) #expect(state.availableFilters == [.favourites])
state.activateFilter(.favourites) state.activateFilter(.favourites)
XCTAssertEqual(state.activeFilters, [.people, .unreads, .favourites]) #expect(state.activeFilters == [.people, .unreads, .favourites])
XCTAssertEqual(state.availableFilters, []) #expect(state.availableFilters == [])
state.clearFilters() state.clearFilters()
XCTAssertFalse(state.isFiltering) #expect(!state.isFiltering)
XCTAssertEqual(state.activeFilters, []) #expect(state.activeFilters == [])
XCTAssertEqual(state.availableFilters, allCasesWithoutLowPriority) #expect(state.availableFilters == allCasesWithoutLowPriority)
} }
func testOrder() { @Test
func order() {
state.activateFilter(.favourites) state.activateFilter(.favourites)
XCTAssertEqual(state.activeFilters, [.favourites]) #expect(state.activeFilters == [.favourites])
XCTAssertEqual(state.availableFilters, [.unreads, .people, .rooms]) #expect(state.availableFilters == [.unreads, .people, .rooms])
state.deactivateFilter(.favourites) state.deactivateFilter(.favourites)
XCTAssertEqual(state.activeFilters, []) #expect(state.activeFilters == [])
XCTAssertEqual(state.availableFilters, allCasesWithoutLowPriority) #expect(state.availableFilters == allCasesWithoutLowPriority)
state.activateFilter(.rooms) state.activateFilter(.rooms)
XCTAssertEqual(state.activeFilters, [.rooms]) #expect(state.activeFilters == [.rooms])
XCTAssertEqual(state.availableFilters, [.unreads, .favourites]) #expect(state.availableFilters == [.unreads, .favourites])
state.activateFilter(.unreads) state.activateFilter(.unreads)
XCTAssertEqual(state.activeFilters, [.rooms, .unreads]) #expect(state.activeFilters == [.rooms, .unreads])
XCTAssertEqual(state.availableFilters, [.favourites]) #expect(state.availableFilters == [.favourites])
state.deactivateFilter(.unreads) state.deactivateFilter(.unreads)
XCTAssertEqual(state.activeFilters, [.rooms]) #expect(state.activeFilters == [.rooms])
XCTAssertEqual(state.availableFilters, [.unreads, .favourites]) #expect(state.availableFilters == [.unreads, .favourites])
} }
// MARK: Low Priority feature flag // MARK: Low Priority feature flag
/// Don't forget to add .lowPriority into the mix above when enabling the feature. /// Don't forget to add .lowPriority into the mix above when enabling the feature.
func testWithLowPriorityFeature() { @Test
func withLowPriorityFeature() {
enableLowPriorityFeature() enableLowPriorityFeature()
XCTAssertFalse(state.isFiltering) #expect(!state.isFiltering)
XCTAssertEqual(state.activeFilters, []) #expect(state.activeFilters == [])
XCTAssertEqual(state.availableFilters, RoomListFilter.allCases) #expect(state.availableFilters == RoomListFilter.allCases)
state.activateFilter(.lowPriority) state.activateFilter(.lowPriority)
XCTAssertEqual(state.activeFilters, [.lowPriority]) #expect(state.activeFilters == [.lowPriority])
XCTAssertEqual(state.availableFilters, [.unreads, .people, .rooms]) #expect(state.availableFilters == [.unreads, .people, .rooms])
} }
// MARK: - Helpers // MARK: - Helpers

View File

@@ -7,10 +7,11 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class RoomMemberDetailsViewModelTests: XCTestCase { @Suite
struct RoomMemberDetailsViewModelTests {
var viewModel: RoomMemberDetailsScreenViewModelProtocol! var viewModel: RoomMemberDetailsScreenViewModelProtocol!
var roomProxyMock: JoinedRoomProxyMock! var roomProxyMock: JoinedRoomProxyMock!
var roomMemberProxyMock: RoomMemberProxyMock! var roomMemberProxyMock: RoomMemberProxyMock!
@@ -18,202 +19,154 @@ class RoomMemberDetailsViewModelTests: XCTestCase {
viewModel.context viewModel.context
} }
override func setUp() async throws { @Test
roomProxyMock = JoinedRoomProxyMock(.init(name: "")) mutating func initialState() async throws {
setup(roomMemberProxyMock: .mockAlice)
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)
let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil } let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil }
try await waitForMemberToLoad.fulfill() try await waitForMemberToLoad.fulfill()
XCTAssertEqual(context.viewState.memberDetails, RoomMemberDetails(withProxy: roomMemberProxyMock)) #expect(context.viewState.memberDetails == RoomMemberDetails(withProxy: roomMemberProxyMock))
XCTAssertNil(context.ignoreUserAlert) #expect(context.ignoreUserAlert == nil)
XCTAssertNil(context.alertInfo) #expect(context.alertInfo == nil)
} }
func testIgnoreSuccess() async throws { @Test
roomMemberProxyMock = RoomMemberProxyMock.mockAlice mutating func ignoreSuccess() async throws {
viewModel = RoomMemberDetailsScreenViewModel(userID: roomMemberProxyMock.userID, setup(roomMemberProxyMock: .mockAlice)
roomProxy: roomProxyMock,
userSession: UserSessionMock(.init()),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
analytics: ServiceLocator.shared.analytics)
let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil } let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil }
try await waitForMemberToLoad.fulfill() try await waitForMemberToLoad.fulfill()
context.send(viewAction: .showIgnoreAlert) context.send(viewAction: .showIgnoreAlert)
XCTAssertEqual(context.ignoreUserAlert, .init(action: .ignore)) #expect(context.ignoreUserAlert == .init(action: .ignore))
context.send(viewAction: .ignoreConfirmed) context.send(viewAction: .ignoreConfirmed)
let deferred = deferFulfillment(context.$viewState) { state in let deferred = deferFulfillment(context.$viewState) { state in
state.memberDetails?.isIgnored == true state.memberDetails?.isIgnored == true
} }
try await deferred.fulfill() try await deferred.fulfill()
guard let memberDetails = context.viewState.memberDetails else { let memberDetails = try #require(context.viewState.memberDetails,
XCTFail("Member details should be loaded at this point") "Member details should be loaded at this point")
return #expect(memberDetails.isIgnored)
} #expect(!context.viewState.isProcessingIgnoreRequest)
XCTAssertTrue(memberDetails.isIgnored)
XCTAssertFalse(context.viewState.isProcessingIgnoreRequest)
try await Task.sleep(for: .milliseconds(100)) try await Task.sleep(for: .milliseconds(100))
XCTAssertTrue(roomProxyMock.updateMembersCalled) #expect(roomProxyMock.updateMembersCalled)
} }
func testIgnoreFailure() async throws { @Test
roomMemberProxyMock = RoomMemberProxyMock.mockAlice mutating func ignoreFailure() async throws {
let clientProxy = ClientProxyMock(.init()) let clientProxy = ClientProxyMock(.init())
clientProxy.ignoreUserReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) clientProxy.ignoreUserReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
viewModel = RoomMemberDetailsScreenViewModel(userID: roomMemberProxyMock.userID, setup(roomMemberProxyMock: .mockAlice, clientProxy: clientProxy)
roomProxy: roomProxyMock,
userSession: UserSessionMock(.init(clientProxy: clientProxy)),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
analytics: ServiceLocator.shared.analytics)
let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil } let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil }
try await waitForMemberToLoad.fulfill() try await waitForMemberToLoad.fulfill()
context.send(viewAction: .showIgnoreAlert) context.send(viewAction: .showIgnoreAlert)
XCTAssertEqual(context.ignoreUserAlert, .init(action: .ignore)) #expect(context.ignoreUserAlert == .init(action: .ignore))
context.send(viewAction: .ignoreConfirmed) context.send(viewAction: .ignoreConfirmed)
let deferred = deferFulfillment(context.$viewState) { state in let deferred = deferFulfillment(context.$viewState) { state in
state.bindings.alertInfo != nil state.bindings.alertInfo != nil
} }
try await deferred.fulfill() try await deferred.fulfill()
guard let memberDetails = context.viewState.memberDetails else { let memberDetails = try #require(context.viewState.memberDetails,
XCTFail("Member details should be loaded at this point") "Member details should be loaded at this point")
return #expect(!memberDetails.isIgnored)
} #expect(context.alertInfo != nil)
XCTAssertFalse(memberDetails.isIgnored)
XCTAssertNotNil(context.alertInfo)
try await Task.sleep(for: .milliseconds(100)) try await Task.sleep(for: .milliseconds(100))
XCTAssertFalse(roomProxyMock.updateMembersCalled) #expect(!roomProxyMock.updateMembersCalled)
} }
func testUnignoreSuccess() async throws { @Test
roomMemberProxyMock = RoomMemberProxyMock.mockIgnored mutating func unignoreSuccess() async throws {
setup(roomMemberProxyMock: .mockIgnored)
viewModel = RoomMemberDetailsScreenViewModel(userID: roomMemberProxyMock.userID,
roomProxy: roomProxyMock,
userSession: UserSessionMock(.init()),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
analytics: ServiceLocator.shared.analytics)
let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil } let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil }
try await waitForMemberToLoad.fulfill() try await waitForMemberToLoad.fulfill()
context.send(viewAction: .showUnignoreAlert) context.send(viewAction: .showUnignoreAlert)
XCTAssertEqual(context.ignoreUserAlert, .init(action: .unignore)) #expect(context.ignoreUserAlert == .init(action: .unignore))
context.send(viewAction: .unignoreConfirmed) context.send(viewAction: .unignoreConfirmed)
let deferred = deferFulfillment(context.$viewState) { state in let deferred = deferFulfillment(context.$viewState) { state in
state.memberDetails?.isIgnored == false state.memberDetails?.isIgnored == false
} }
try await deferred.fulfill() try await deferred.fulfill()
guard let memberDetails = context.viewState.memberDetails else { let memberDetails = try #require(context.viewState.memberDetails,
XCTFail("Member details should be loaded at this point") "Member details should be loaded at this point")
return #expect(!memberDetails.isIgnored)
}
XCTAssertFalse(memberDetails.isIgnored)
try await Task.sleep(for: .milliseconds(100)) try await Task.sleep(for: .milliseconds(100))
XCTAssertTrue(roomProxyMock.updateMembersCalled) #expect(roomProxyMock.updateMembersCalled)
} }
func testUnignoreFailure() async throws { @Test
roomMemberProxyMock = RoomMemberProxyMock.mockIgnored mutating func unignoreFailure() async throws {
let clientProxy = ClientProxyMock(.init()) let clientProxy = ClientProxyMock(.init())
clientProxy.unignoreUserReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) clientProxy.unignoreUserReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
viewModel = RoomMemberDetailsScreenViewModel(userID: roomMemberProxyMock.userID, setup(roomMemberProxyMock: .mockIgnored, clientProxy: clientProxy)
roomProxy: roomProxyMock,
userSession: UserSessionMock(.init(clientProxy: clientProxy)),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
analytics: ServiceLocator.shared.analytics)
let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil } let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil }
try await waitForMemberToLoad.fulfill() try await waitForMemberToLoad.fulfill()
context.send(viewAction: .showUnignoreAlert) context.send(viewAction: .showUnignoreAlert)
XCTAssertEqual(context.ignoreUserAlert, .init(action: .unignore)) #expect(context.ignoreUserAlert == .init(action: .unignore))
context.send(viewAction: .unignoreConfirmed) context.send(viewAction: .unignoreConfirmed)
let deferred = deferFulfillment(context.$viewState) { state in let deferred = deferFulfillment(context.$viewState) { state in
state.bindings.alertInfo != nil state.bindings.alertInfo != nil
} }
try await deferred.fulfill() try await deferred.fulfill()
guard let memberDetails = context.viewState.memberDetails else { let memberDetails = try #require(context.viewState.memberDetails,
XCTFail("Member details should be loaded at this point") "Member details should be loaded at this point")
return #expect(memberDetails.isIgnored)
} #expect(context.alertInfo != nil)
XCTAssertTrue(memberDetails.isIgnored)
XCTAssertNotNil(context.alertInfo)
try await Task.sleep(for: .milliseconds(100)) try await Task.sleep(for: .milliseconds(100))
XCTAssertFalse(roomProxyMock.updateMembersCalled) #expect(!roomProxyMock.updateMembersCalled)
} }
func testInitialStateAccountOwner() async throws { @Test
roomMemberProxyMock = RoomMemberProxyMock.mockMe mutating func initialStateAccountOwner() async throws {
viewModel = RoomMemberDetailsScreenViewModel(userID: roomMemberProxyMock.userID, setup(roomMemberProxyMock: .mockMe)
roomProxy: roomProxyMock,
userSession: UserSessionMock(.init()),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
analytics: ServiceLocator.shared.analytics)
let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil } let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil }
try await waitForMemberToLoad.fulfill() try await waitForMemberToLoad.fulfill()
XCTAssertEqual(context.viewState.memberDetails, RoomMemberDetails(withProxy: roomMemberProxyMock)) #expect(context.viewState.memberDetails == RoomMemberDetails(withProxy: roomMemberProxyMock))
XCTAssertNil(context.ignoreUserAlert) #expect(context.ignoreUserAlert == nil)
XCTAssertNil(context.alertInfo) #expect(context.alertInfo == nil)
} }
func testInitialStateIgnoredUser() async throws { @Test
roomMemberProxyMock = RoomMemberProxyMock.mockIgnored mutating func initialStateIgnoredUser() async throws {
viewModel = RoomMemberDetailsScreenViewModel(userID: roomMemberProxyMock.userID, setup(roomMemberProxyMock: .mockIgnored)
roomProxy: roomProxyMock,
userSession: UserSessionMock(.init()),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
analytics: ServiceLocator.shared.analytics)
let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil } let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil }
try await waitForMemberToLoad.fulfill() try await waitForMemberToLoad.fulfill()
XCTAssertEqual(context.viewState.memberDetails, RoomMemberDetails(withProxy: roomMemberProxyMock)) #expect(context.viewState.memberDetails == RoomMemberDetails(withProxy: roomMemberProxyMock))
XCTAssertNil(context.ignoreUserAlert) #expect(context.ignoreUserAlert == nil)
XCTAssertNil(context.alertInfo) #expect(context.alertInfo == nil)
}
// MARK: - Helpers
private mutating func setup(roomMemberProxyMock: RoomMemberProxyMock, clientProxy: ClientProxyMock? = nil) {
self.roomMemberProxyMock = roomMemberProxyMock
roomProxyMock = JoinedRoomProxyMock(.init(name: ""))
roomProxyMock.getMemberUserIDClosure = { _ in
.success(roomMemberProxyMock)
}
// swiftlint:disable:next force_unwrapping
let userSession = clientProxy != nil ? UserSessionMock(.init(clientProxy: clientProxy!)) : UserSessionMock(.init())
viewModel = RoomMemberDetailsScreenViewModel(userID: roomMemberProxyMock.userID,
roomProxy: roomProxyMock,
userSession: userSession,
userIndicatorController: ServiceLocator.shared.userIndicatorController,
analytics: ServiceLocator.shared.analytics)
} }
} }

View File

@@ -7,22 +7,24 @@
import Combine import Combine
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class RoomMembersFlowCoordinatorTests: XCTestCase { @Suite
struct RoomMembersFlowCoordinatorTests {
var membersFlowCoordinator: RoomMembersFlowCoordinator! var membersFlowCoordinator: RoomMembersFlowCoordinator!
var navigationStackCoordinator: NavigationStackCoordinator! var navigationStackCoordinator: NavigationStackCoordinator!
var stateMachineFactory: PublishedStateMachineFactory! var stateMachineFactory: PublishedStateMachineFactory!
func testClearRoute() async throws { @Test
try await setUp(entryPoint: .roomMembersList) mutating func clearRoute() async throws {
XCTAssertTrue(navigationStackCoordinator.stackCoordinators.last is RoomMembersListScreenCoordinator) try await setup(entryPoint: .roomMembersList)
#expect(navigationStackCoordinator.stackCoordinators.last is RoomMembersListScreenCoordinator)
var membersFlowStateExpectation = deferFulfillment(stateMachineFactory.membersFlowStatePublisher) { $0 == .roomMemberDetails(userID: "test", previousState: .roomMembersList) } var membersFlowStateExpectation = deferFulfillment(stateMachineFactory.membersFlowStatePublisher) { $0 == .roomMemberDetails(userID: "test", previousState: .roomMembersList) }
membersFlowCoordinator.handleAppRoute(.roomMemberDetails(userID: "test"), animated: false) membersFlowCoordinator.handleAppRoute(.roomMemberDetails(userID: "test"), animated: false)
try await membersFlowStateExpectation.fulfill() try await membersFlowStateExpectation.fulfill()
XCTAssertTrue(navigationStackCoordinator.stackCoordinators.last is RoomMemberDetailsScreenCoordinator) #expect(navigationStackCoordinator.stackCoordinators.last is RoomMemberDetailsScreenCoordinator)
membersFlowStateExpectation = deferFulfillment(stateMachineFactory.membersFlowStatePublisher) { $0 == .roomMembersList } membersFlowStateExpectation = deferFulfillment(stateMachineFactory.membersFlowStatePublisher) { $0 == .roomMembersList }
let membersFlowActionExpectation = deferFulfillment(membersFlowCoordinator.actions) { action in let membersFlowActionExpectation = deferFulfillment(membersFlowCoordinator.actions) { action in
@@ -36,10 +38,12 @@ class RoomMembersFlowCoordinatorTests: XCTestCase {
membersFlowCoordinator.clearRoute(animated: false) membersFlowCoordinator.clearRoute(animated: false)
try await membersFlowStateExpectation.fulfill() try await membersFlowStateExpectation.fulfill()
try await membersFlowActionExpectation.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() stateMachineFactory = .init()
navigationStackCoordinator = NavigationStackCoordinator() navigationStackCoordinator = NavigationStackCoordinator()
navigationStackCoordinator.setRootCoordinator(PlaceholderScreenCoordinator(hideBrandChrome: false)) navigationStackCoordinator.setRootCoordinator(PlaceholderScreenCoordinator(hideBrandChrome: false))

View File

@@ -8,42 +8,37 @@
import Combine import Combine
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class RoomMembersListScreenViewModelTests: XCTestCase { @Suite
struct RoomMembersListScreenViewModelTests {
var viewModel: RoomMembersListScreenViewModel! var viewModel: RoomMembersListScreenViewModel!
var roomProxy: JoinedRoomProxyMock! var roomProxy: JoinedRoomProxyMock!
var context: RoomMembersListScreenViewModel.Context { var context: RoomMembersListScreenViewModel.Context {
viewModel.context viewModel.context
} }
override func tearDown() { @Test
viewModel = nil mutating func joinedMembers() async throws {
roomProxy = nil setup(members: [.mockAlice, .mockBob])
}
func testJoinedMembers() async throws {
setup(with: [.mockAlice, .mockBob])
let deferred = deferFulfillment(context.$viewState) { state in let deferred = deferFulfillment(context.$viewState) { state in
state.visibleJoinedMembers.count == 2 state.visibleJoinedMembers.count == 2
} }
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(viewModel.state.joinedMembersCount, 2) #expect(viewModel.state.joinedMembersCount == 2)
XCTAssertEqual(viewModel.state.visibleJoinedMembers.count, 2) #expect(viewModel.state.visibleJoinedMembers.count == 2)
} }
func testSortingMembers() async throws { @Test
setup(with: [.mockModerator, .mockDan, .mockAlice, .mockAdmin]) mutating func sortingMembers() async throws {
setup(members: [.mockModerator, .mockDan, .mockAlice, .mockAdmin])
let deferred = deferFulfillment(context.$viewState) { state in let deferred = deferFulfillment(context.$viewState) { state in
state.visibleJoinedMembers.count == 4 state.visibleJoinedMembers.count == 4
} }
try await deferred.fulfill() try await deferred.fulfill()
let sortedMembers: [RoomMemberListScreenEntry] = [ let sortedMembers: [RoomMemberListScreenEntry] = [
@@ -57,231 +52,236 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
verificationState: .notVerified) verificationState: .notVerified)
] ]
XCTAssertEqual(viewModel.state.visibleJoinedMembers, sortedMembers) #expect(viewModel.state.visibleJoinedMembers == sortedMembers)
} }
func testSearch() async throws { @Test
setup(with: [.mockAlice, .mockBob]) mutating func search() async throws {
setup(members: [.mockAlice, .mockBob])
let deferred = deferFulfillment(context.$viewState) { state in let deferred = deferFulfillment(context.$viewState) { state in
state.visibleJoinedMembers.count == 1 state.visibleJoinedMembers.count == 1
} }
context.searchQuery = "alice" context.searchQuery = "alice"
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(viewModel.state.joinedMembersCount, 2) #expect(viewModel.state.joinedMembersCount == 2)
XCTAssertEqual(viewModel.state.visibleJoinedMembers.count, 1) #expect(viewModel.state.visibleJoinedMembers.count == 1)
} }
func testEmptySearch() async throws { @Test
setup(with: [.mockAlice, .mockBob]) mutating func emptySearch() async throws {
context.searchQuery = "WWW" setup(members: [.mockAlice, .mockBob])
context.searchQuery = "WWW"
let deferred = deferFulfillment(context.$viewState) { state in let deferred = deferFulfillment(context.$viewState) { state in
state.joinedMembersCount == 2 state.joinedMembersCount == 2
} }
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(viewModel.state.joinedMembersCount, 2) #expect(viewModel.state.joinedMembersCount == 2)
XCTAssertEqual(viewModel.state.visibleJoinedMembers.count, 0) #expect(viewModel.state.visibleJoinedMembers.count == 0)
} }
func testJoinedAndInvitedMembers() async throws { @Test
setup(with: [.mockInvited, .mockBob]) mutating func joinedAndInvitedMembers() async throws {
setup(members: [.mockInvited, .mockBob])
let deferred = deferFulfillment(context.$viewState) { state in let deferred = deferFulfillment(context.$viewState) { state in
state.visibleInvitedMembers.count == 1 state.visibleInvitedMembers.count == 1
} }
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(viewModel.state.joinedMembersCount, 1) #expect(viewModel.state.joinedMembersCount == 1)
XCTAssertEqual(viewModel.state.visibleInvitedMembers.count, 1) #expect(viewModel.state.visibleInvitedMembers.count == 1)
XCTAssertEqual(viewModel.state.visibleJoinedMembers.count, 1) #expect(viewModel.state.visibleJoinedMembers.count == 1)
} }
func testInvitedMembers() async throws { @Test
setup(with: [.mockInvited]) mutating func invitedMembers() async throws {
setup(members: [.mockInvited])
let deferred = deferFulfillment(context.$viewState) { state in let deferred = deferFulfillment(context.$viewState) { state in
state.visibleInvitedMembers.count == 1 state.visibleInvitedMembers.count == 1
} }
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(viewModel.state.joinedMembersCount, 0) #expect(viewModel.state.joinedMembersCount == 0)
XCTAssertEqual(viewModel.state.visibleInvitedMembers.count, 1) #expect(viewModel.state.visibleInvitedMembers.count == 1)
XCTAssertEqual(viewModel.state.visibleJoinedMembers.count, 0) #expect(viewModel.state.visibleJoinedMembers.count == 0)
} }
func testSearchInvitedMembers() async throws { @Test
setup(with: [.mockInvited]) mutating func searchInvitedMembers() async throws {
setup(members: [.mockInvited])
context.searchQuery = "invited" context.searchQuery = "invited"
let deferred = deferFulfillment(context.$viewState) { state in let deferred = deferFulfillment(context.$viewState) { state in
state.visibleInvitedMembers.count == 1 state.visibleInvitedMembers.count == 1
} }
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(viewModel.state.joinedMembersCount, 0) #expect(viewModel.state.joinedMembersCount == 0)
XCTAssertEqual(viewModel.state.visibleInvitedMembers.count, 1) #expect(viewModel.state.visibleInvitedMembers.count == 1)
XCTAssertEqual(viewModel.state.visibleJoinedMembers.count, 0) #expect(viewModel.state.visibleJoinedMembers.count == 0)
} }
func testSelectUserAsUser() async throws { @Test
// Given the room list viewed as a regular user. mutating func selectUserAsUser() async throws {
setup(with: .allMembers) setup(members: .allMembers)
var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty } var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty }
try await deferred.fulfill() try await deferred.fulfill()
// When tapping on another user in the list.
deferred = deferFulfillment(context.$viewState) { $0.bindings.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 { 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 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.")
return
}
context.send(viewAction: .selectMember(user)) context.send(viewAction: .selectMember(user))
try await deferred.fulfill() try await deferred.fulfill()
// Then member management should be shown for that user. #expect(context.manageMemeberViewModel != nil)
XCTAssertEqual(context.manageMemeberViewModel?.state.memberDetails.id, user.id) #expect(context.manageMemeberViewModel?.state.memberDetails.id == user.id)
XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canKick, true) #expect(context.manageMemeberViewModel?.state.permissions.canKick == false)
XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canBan, true) #expect(context.manageMemeberViewModel?.state.permissions.canBan == false)
XCTAssertEqual(context.manageMemeberViewModel?.state.isKickDisabled, false)
XCTAssertEqual(context.manageMemeberViewModel?.state.isBanUnbanDisabled, false)
XCTAssertEqual(context.manageMemeberViewModel?.state.isMemberBanned, false)
} }
func testSelectModeratorAsAdmin() async throws { @Test
// Given the room list viewed as an admin. mutating func selectUserAsAdmin() async throws {
setup(with: .allMembersAsAdmin) setup(members: .allMembersAsAdmin)
var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers } var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers }
try await deferred.fulfill() 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 } deferred = deferFulfillment(context.$viewState) { $0.bindings.manageMemeberViewModel != nil }
guard let moderator = viewModel.state.visibleJoinedMembers.first(where: { $0.member.role == .moderator })?.member else { 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 return
} }
context.send(viewAction: .selectMember(moderator)) context.send(viewAction: .selectMember(moderator))
try await deferred.fulfill() try await deferred.fulfill()
// Then member management should be shown for the moderator. #expect(context.manageMemeberViewModel?.state.memberDetails.id == moderator.id)
XCTAssertEqual(context.manageMemeberViewModel?.state.memberDetails.id, moderator.id) #expect(context.manageMemeberViewModel?.state.permissions.canKick == true)
XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canKick, true) #expect(context.manageMemeberViewModel?.state.permissions.canBan == true)
XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canBan, true) #expect(context.manageMemeberViewModel?.state.isMemberBanned == false)
XCTAssertEqual(context.manageMemeberViewModel?.state.isMemberBanned, false) #expect(context.manageMemeberViewModel?.state.isKickDisabled == false)
XCTAssertEqual(context.manageMemeberViewModel?.state.isKickDisabled, false) #expect(context.manageMemeberViewModel?.state.isBanUnbanDisabled == false)
XCTAssertEqual(context.manageMemeberViewModel?.state.isBanUnbanDisabled, false)
} }
func testSelectAdminAsAdmin() async throws { @Test
// Given the room list viewed as an admin. mutating func selectAdminAsAdmin() async throws {
setup(with: .allMembersAsAdmin) setup(members: .allMembersAsAdmin)
var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers } var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers }
try await deferred.fulfill() try await deferred.fulfill()
// When tapping on another administrator in the list.
deferred = deferFulfillment(context.$viewState) { $0.bindings.manageMemeberViewModel != nil } 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 { 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 return
} }
context.send(viewAction: .selectMember(admin))
// Then the administrator's details should be shown. context.send(viewAction: .selectMember(admin))
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(context.manageMemeberViewModel?.state.memberDetails.id, admin.id)
XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canKick, true) #expect(context.manageMemeberViewModel?.state.memberDetails.id == admin.id)
XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canBan, true) #expect(context.manageMemeberViewModel?.state.permissions.canKick == true)
XCTAssertEqual(context.manageMemeberViewModel?.state.isKickDisabled, true) #expect(context.manageMemeberViewModel?.state.permissions.canBan == true)
XCTAssertEqual(context.manageMemeberViewModel?.state.isBanUnbanDisabled, true) #expect(context.manageMemeberViewModel?.state.isKickDisabled == true)
XCTAssertEqual(context.manageMemeberViewModel?.state.isMemberBanned, false) #expect(context.manageMemeberViewModel?.state.isBanUnbanDisabled == true)
#expect(context.manageMemeberViewModel?.state.isMemberBanned == false)
} }
func testSelectOwnMemberAsAdmin() async throws { @Test
// Given the room list viewed as an admin. mutating func selectOwnMemberAsAdmin() async throws {
setup(with: .allMembersAsAdmin) setup(members: .allMembersAsAdmin)
let deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty } let deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty }
try await deferred.fulfill() try await deferred.fulfill()
// When tapping on yourself in the list.
let memberDetailsAction = deferFulfillment(viewModel.actions) { $0.isSelectMember } let memberDetailsAction = deferFulfillment(viewModel.actions) { $0.isSelectMember }
guard let ownMember = viewModel.state.visibleJoinedMembers.first(where: { $0.member.id == RoomMemberProxyMock.mockMe.userID })?.member else { 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 return
} }
context.send(viewAction: .selectMember(ownMember))
// Then your member's details should be shown. context.send(viewAction: .selectMember(ownMember))
try await memberDetailsAction.fulfill() try await memberDetailsAction.fulfill()
XCTAssertNil(context.manageMemeberViewModel)
#expect(context.manageMemeberViewModel == nil)
} }
func testSelectBannedMember() async throws { @Test
// Given the room list viewed as an admin. mutating func selectBannedMember() async throws {
setup(with: .allMembersAsAdmin + RoomMemberProxyMock.mockBanned) setup(members: .allMembersAsAdmin + RoomMemberProxyMock.mockBanned)
var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers } var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers }
try await deferred.fulfill() 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 } deferred = deferFulfillment(context.$viewState) { $0.bindings.manageMemeberViewModel != nil }
guard let bannedMember = viewModel.state.visibleBannedMembers.first?.member else { 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 return
} }
context.send(viewAction: .selectMember(bannedMember))
// Then an alert should be shown to unban the user. context.send(viewAction: .selectMember(bannedMember))
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(context.manageMemeberViewModel?.state.memberDetails.id, bannedMember.id)
XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canKick, true) #expect(context.manageMemeberViewModel?.state.memberDetails.id == bannedMember.id)
XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canBan, true) #expect(context.manageMemeberViewModel?.state.permissions.canKick == true)
XCTAssertEqual(context.manageMemeberViewModel?.state.isKickDisabled, true) #expect(context.manageMemeberViewModel?.state.permissions.canBan == true)
XCTAssertEqual(context.manageMemeberViewModel?.state.isBanUnbanDisabled, false) #expect(context.manageMemeberViewModel?.state.isKickDisabled == true)
XCTAssertEqual(context.manageMemeberViewModel?.state.isMemberBanned, true) #expect(context.manageMemeberViewModel?.state.isBanUnbanDisabled == false)
#expect(context.manageMemeberViewModel?.state.isMemberBanned == true)
} }
func testSwitchesToMembersModeWhenThereAreNoBannedMembers() async throws { @Test
// Given the room list viewed as an admin. mutating func switchesToMembersModeWhenThereAreNoBannedMembers() async throws {
roomProxy = JoinedRoomProxyMock(.init(name: "test")) roomProxy = JoinedRoomProxyMock(.init(name: "test"))
let subject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([RoomMemberProxyMock].allMembersAsAdmin + RoomMemberProxyMock.mockBanned) let subject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([RoomMemberProxyMock].allMembersAsAdmin + RoomMemberProxyMock.mockBanned)
roomProxy.membersPublisher = subject.asCurrentValuePublisher() roomProxy.membersPublisher = subject.asCurrentValuePublisher()
viewModel = .init(userSession: UserSessionMock(.init()),
roomProxy: roomProxy, viewModel = RoomMembersListScreenViewModel(userSession: UserSessionMock(.init()),
userIndicatorController: ServiceLocator.shared.userIndicatorController, roomProxy: roomProxy,
analytics: ServiceLocator.shared.analytics) 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 } var deferred = deferFulfillment(context.$viewState) { $0.visibleBannedMembers.count == 4 && $0.bindings.mode == .banned }
context.mode = .banned context.mode = .banned
@@ -292,11 +292,13 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
try await deferred.fulfill() 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)) roomProxy = JoinedRoomProxyMock(.init(name: "test", members: members))
viewModel = .init(userSession: UserSessionMock(.init()), viewModel = RoomMembersListScreenViewModel(userSession: UserSessionMock(.init()),
roomProxy: roomProxy, roomProxy: roomProxy,
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
analytics: ServiceLocator.shared.analytics) analytics: ServiceLocator.shared.analytics)
} }
} }

View File

@@ -8,10 +8,12 @@
@testable import ElementX @testable import ElementX
import MatrixRustSDK import MatrixRustSDK
import XCTest import Testing
class RoomPermissionsTests: XCTestCase { @Suite
func testFromRust() { struct RoomPermissionsTests {
@Test
func fromRust() {
// Given a set of power level changes with various values. // Given a set of power level changes with various values.
let powerLevels = RoomPowerLevelsValues(ban: 100, let powerLevels = RoomPowerLevelsValues(ban: 100,
invite: 100, invite: 100,
@@ -29,16 +31,16 @@ class RoomPermissionsTests: XCTestCase {
let permissions = RoomPermissions(powerLevels: powerLevels) let permissions = RoomPermissions(powerLevels: powerLevels)
// Then the permissions should be created with values mapped to the correct role. // Then the permissions should be created with values mapped to the correct role.
XCTAssertEqual(permissions.ban, RoomRole.administrator.powerLevelValue) #expect(permissions.ban == RoomRole.administrator.powerLevelValue)
XCTAssertEqual(permissions.invite, RoomRole.administrator.powerLevelValue) #expect(permissions.invite == RoomRole.administrator.powerLevelValue)
XCTAssertEqual(permissions.kick, RoomRole.administrator.powerLevelValue) #expect(permissions.kick == RoomRole.administrator.powerLevelValue)
XCTAssertEqual(permissions.redact, RoomRole.moderator.powerLevelValue) #expect(permissions.redact == RoomRole.moderator.powerLevelValue)
XCTAssertEqual(permissions.eventsDefault, RoomRole.moderator.powerLevelValue) #expect(permissions.eventsDefault == RoomRole.moderator.powerLevelValue)
XCTAssertEqual(permissions.stateDefault, RoomRole.moderator.powerLevelValue) #expect(permissions.stateDefault == RoomRole.moderator.powerLevelValue)
XCTAssertEqual(permissions.usersDefault, RoomRole.user.powerLevelValue) #expect(permissions.usersDefault == RoomRole.user.powerLevelValue)
XCTAssertEqual(permissions.roomName, RoomRole.user.powerLevelValue) #expect(permissions.roomName == RoomRole.user.powerLevelValue)
XCTAssertEqual(permissions.roomAvatar, RoomRole.user.powerLevelValue) #expect(permissions.roomAvatar == RoomRole.user.powerLevelValue)
XCTAssertEqual(permissions.roomTopic, RoomRole.user.powerLevelValue) #expect(permissions.roomTopic == RoomRole.user.powerLevelValue)
XCTAssertEqual(permissions.spaceChild, RoomRole.administrator.powerLevelValue) #expect(permissions.spaceChild == RoomRole.administrator.powerLevelValue)
} }
} }

View File

@@ -7,10 +7,11 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class RoomRolesAndPermissionsScreenViewModelTests: XCTestCase { @Suite
struct RoomRolesAndPermissionsScreenViewModelTests {
var viewModel: RoomRolesAndPermissionsScreenViewModelProtocol! var viewModel: RoomRolesAndPermissionsScreenViewModelProtocol!
var roomProxy: JoinedRoomProxyMock! var roomProxy: JoinedRoomProxyMock!
@@ -18,62 +19,69 @@ class RoomRolesAndPermissionsScreenViewModelTests: XCTestCase {
viewModel.context viewModel.context
} }
func testEmptyCounters() { @Test
setupViewModel(members: .allMembers) mutating func emptyCounters() {
XCTAssertEqual(context.viewState.administratorCount, 0) setup(members: .allMembers)
XCTAssertEqual(context.viewState.moderatorCount, 0)
#expect(context.viewState.administratorCount == 0)
#expect(context.viewState.moderatorCount == 0)
} }
func testFilledCounters() { @Test
setupViewModel(members: .allMembersAsAdmin) mutating func filledCounters() {
XCTAssertEqual(context.viewState.administratorCount, 2) setup(members: .allMembersAsAdmin)
XCTAssertEqual(context.viewState.moderatorCount, 1)
#expect(context.viewState.administratorCount == 2)
#expect(context.viewState.moderatorCount == 1)
} }
func testResetPermissions() async throws { @Test
setupViewModel(members: .allMembersAsAdmin) mutating func resetPermissions() async throws {
setup(members: .allMembersAsAdmin)
context.send(viewAction: .reset) context.send(viewAction: .reset)
XCTAssertNotNil(context.alertInfo) #expect(context.alertInfo != nil)
context.alertInfo?.primaryButton.action?() context.alertInfo?.primaryButton.action?()
try await Task.sleep(for: .milliseconds(100)) try await Task.sleep(for: .milliseconds(100))
XCTAssertTrue(roomProxy.resetPowerLevelsCalled) #expect(roomProxy.resetPowerLevelsCalled)
} }
func testDemoteToModerator() async throws { @Test
setupViewModel(members: .allMembersAsAdmin) mutating func demoteToModerator() async throws {
setup(members: .allMembersAsAdmin)
context.send(viewAction: .editOwnUserRole) context.send(viewAction: .editOwnUserRole)
XCTAssertNotNil(context.alertInfo) #expect(context.alertInfo != nil)
context.alertInfo?.verticalButtons?.first { $0.title.localizedStandardContains("moderator") }?.action?() context.alertInfo?.verticalButtons?.first { $0.title.localizedStandardContains("moderator") }?.action?()
try await Task.sleep(for: .milliseconds(100)) try await Task.sleep(for: .milliseconds(100))
XCTAssertTrue(roomProxy.updatePowerLevelsForUsersCalled) #expect(roomProxy.updatePowerLevelsForUsersCalled)
XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.first?.powerLevel, #expect(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.first?.powerLevel == RoomRole.moderator.powerLevelValue)
RoomRole.moderator.powerLevelValue)
} }
func testDemoteToMember() async throws { @Test
setupViewModel(members: .allMembersAsAdmin) mutating func demoteToMember() async throws {
setup(members: .allMembersAsAdmin)
context.send(viewAction: .editOwnUserRole) context.send(viewAction: .editOwnUserRole)
XCTAssertNotNil(context.alertInfo) #expect(context.alertInfo != nil)
context.alertInfo?.verticalButtons?.first { $0.title.localizedStandardContains("member") }?.action?() context.alertInfo?.verticalButtons?.first { $0.title.localizedStandardContains("member") }?.action?()
try await Task.sleep(for: .milliseconds(100)) try await Task.sleep(for: .milliseconds(100))
XCTAssertTrue(roomProxy.updatePowerLevelsForUsersCalled) #expect(roomProxy.updatePowerLevelsForUsersCalled)
XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.first?.powerLevel, #expect(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.first?.powerLevel == RoomRole.user.powerLevelValue)
RoomRole.user.powerLevelValue)
} }
private func setupViewModel(members: [RoomMemberProxyMock]) { // MARK: - Helpers
private mutating func setup(members: [RoomMemberProxyMock]) {
roomProxy = JoinedRoomProxyMock(.init(members: members)) roomProxy = JoinedRoomProxyMock(.init(members: members))
viewModel = RoomRolesAndPermissionsScreenViewModel(roomProxy: roomProxy, viewModel = RoomRolesAndPermissionsScreenViewModel(roomProxy: roomProxy,
userIndicatorController: UserIndicatorControllerMock(), userIndicatorController: UserIndicatorControllerMock(),

View File

@@ -8,20 +8,22 @@
@testable import ElementX @testable import ElementX
import MatrixRustSDK import MatrixRustSDK
import XCTest import Testing
class RoomStateEventStringBuilderTests: XCTestCase { @Suite
var userID: String! struct RoomStateEventStringBuilderTests {
var stringBuilder: RoomStateEventStringBuilder! private let userID: String
private let stringBuilder: RoomStateEventStringBuilder
override func setUp() { init() {
userID = "@alice:matrix.org" userID = "@alice:matrix.org"
stringBuilder = RoomStateEventStringBuilder(userID: userID) stringBuilder = RoomStateEventStringBuilder(userID: userID)
} }
// MARK: - User Profiles // MARK: - User Profiles
func testDisplayNameChanges() { @Test
func displayNameChanges() {
// Changes by you. // Changes by you.
validateDisplayNameChange(senderID: userID, oldName: "Alice", newName: "Bob", validateDisplayNameChange(senderID: userID, oldName: "Alice", newName: "Bob",
expectedString: L10n.stateEventDisplayNameChangedFromByYou("Alice", "Bob")) expectedString: L10n.stateEventDisplayNameChangedFromByYou("Alice", "Bob"))
@@ -40,7 +42,7 @@ class RoomStateEventStringBuilderTests: XCTestCase {
expectedString: L10n.stateEventDisplayNameSet(senderID, "Bob")) 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 sender = TimelineItemSender(id: senderID, displayName: newName)
let string = stringBuilder.buildProfileChangeString(displayName: newName, let string = stringBuilder.buildProfileChangeString(displayName: newName,
previousDisplayName: oldName, previousDisplayName: oldName,
@@ -48,10 +50,11 @@ class RoomStateEventStringBuilderTests: XCTestCase {
previousAvatarURLString: nil, previousAvatarURLString: nil,
member: sender.id, member: sender.id,
memberIsYou: sender.id == userID) memberIsYou: sender.id == userID)
XCTAssertEqual(string, expectedString) #expect(string == expectedString)
} }
func testAvatarChanges() { @Test
func avatarChanges() {
// Changes by you. // Changes by you.
validateAvatarChange(senderID: userID, oldAvatarURL: "mxc://1", newAvatarURL: "mxc://2", validateAvatarChange(senderID: userID, oldAvatarURL: "mxc://1", newAvatarURL: "mxc://2",
expectedString: L10n.stateEventAvatarUrlChangedByYou) expectedString: L10n.stateEventAvatarUrlChangedByYou)
@@ -71,9 +74,9 @@ class RoomStateEventStringBuilderTests: XCTestCase {
expectedString: L10n.stateEventAvatarUrlChanged(senderName)) expectedString: L10n.stateEventAvatarUrlChanged(senderName))
} }
func validateAvatarChange(senderID: String, senderName: String? = nil, private func validateAvatarChange(senderID: String, senderName: String? = nil,
oldAvatarURL: String?, newAvatarURL: String?, oldAvatarURL: String?, newAvatarURL: String?,
expectedString: String) { expectedString: String) {
let sender = TimelineItemSender(id: senderID, displayName: senderName) let sender = TimelineItemSender(id: senderID, displayName: senderName)
let string = stringBuilder.buildProfileChangeString(displayName: senderName, let string = stringBuilder.buildProfileChangeString(displayName: senderName,
previousDisplayName: senderName, previousDisplayName: senderName,
@@ -81,36 +84,38 @@ class RoomStateEventStringBuilderTests: XCTestCase {
previousAvatarURLString: oldAvatarURL, previousAvatarURLString: oldAvatarURL,
member: sender.id, member: sender.id,
memberIsYou: sender.id == userID) memberIsYou: sender.id == userID)
XCTAssertEqual(string, expectedString) #expect(string == expectedString)
} }
// MARK: - Room Info // MARK: - Room Info
func testTopicChanges() { @Test
func topicChanges() {
let you = TimelineItemSender(id: userID, displayName: "Alice") let you = TimelineItemSender(id: userID, displayName: "Alice")
let other = TimelineItemSender(id: "@bob:matrix.org", displayName: "Bob") let other = TimelineItemSender(id: "@bob:matrix.org", displayName: "Bob")
let newTopic = "New topic" let newTopic = "New topic"
var string = stringBuilder.buildString(for: .roomTopic(topic: newTopic), sender: you, isOutgoing: true) 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) 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 = "" let emptyTopic = ""
string = stringBuilder.buildString(for: .roomTopic(topic: emptyTopic), sender: you, isOutgoing: true) 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) 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) 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) 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 // MARK: - Room Membership
func testKickMember() { @Test
func kickMember() {
let you = TimelineItemSender(id: userID, displayName: "Alice") let you = TimelineItemSender(id: userID, displayName: "Alice")
let other = TimelineItemSender(id: "@bob:matrix.org", displayName: "Bob") let other = TimelineItemSender(id: "@bob:matrix.org", displayName: "Bob")
let banned = TimelineItemSender(id: "@spam:matrix.org", displayName: "I like spam") let banned = TimelineItemSender(id: "@spam:matrix.org", displayName: "I like spam")
@@ -122,31 +127,32 @@ class RoomStateEventStringBuilderTests: XCTestCase {
memberDisplayName: banned.displayName, memberDisplayName: banned.displayName,
sender: you, sender: you,
isOutgoing: true) 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, string = stringBuilder.buildString(for: .kicked,
reason: nil, reason: nil,
memberUserID: banned.id, memberUserID: banned.id,
memberDisplayName: banned.displayName, memberDisplayName: banned.displayName,
sender: you, sender: you,
isOutgoing: true) isOutgoing: true)
XCTAssertEqual(string, L10n.stateEventRoomRemoveByYou(banned.displayName ?? banned.id)) #expect(string == L10n.stateEventRoomRemoveByYou(banned.displayName ?? banned.id))
string = stringBuilder.buildString(for: .kicked, string = stringBuilder.buildString(for: .kicked,
reason: reason, reason: reason,
memberUserID: banned.id, memberUserID: banned.id,
memberDisplayName: banned.displayName, memberDisplayName: banned.displayName,
sender: other, sender: other,
isOutgoing: false) 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, string = stringBuilder.buildString(for: .kicked,
reason: nil, reason: nil,
memberUserID: banned.id, memberUserID: banned.id,
memberDisplayName: banned.displayName, memberDisplayName: banned.displayName,
sender: other, sender: other,
isOutgoing: false) 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 you = TimelineItemSender(id: userID, displayName: "Alice")
let other = TimelineItemSender(id: "@bob:matrix.org", displayName: "Bob") let other = TimelineItemSender(id: "@bob:matrix.org", displayName: "Bob")
let banned = TimelineItemSender(id: "@spam:matrix.org", displayName: "I like spam") let banned = TimelineItemSender(id: "@spam:matrix.org", displayName: "I like spam")
@@ -158,27 +164,27 @@ class RoomStateEventStringBuilderTests: XCTestCase {
memberDisplayName: banned.displayName, memberDisplayName: banned.displayName,
sender: you, sender: you,
isOutgoing: true) 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, string = stringBuilder.buildString(for: .banned,
reason: nil, reason: nil,
memberUserID: banned.id, memberUserID: banned.id,
memberDisplayName: banned.displayName, memberDisplayName: banned.displayName,
sender: you, sender: you,
isOutgoing: true) isOutgoing: true)
XCTAssertEqual(string, L10n.stateEventRoomBanByYou(banned.displayName ?? banned.id)) #expect(string == L10n.stateEventRoomBanByYou(banned.displayName ?? banned.id))
string = stringBuilder.buildString(for: .banned, string = stringBuilder.buildString(for: .banned,
reason: reason, reason: reason,
memberUserID: banned.id, memberUserID: banned.id,
memberDisplayName: banned.displayName, memberDisplayName: banned.displayName,
sender: other, sender: other,
isOutgoing: false) 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, string = stringBuilder.buildString(for: .banned,
reason: nil, reason: nil,
memberUserID: banned.id, memberUserID: banned.id,
memberDisplayName: banned.displayName, memberDisplayName: banned.displayName,
sender: other, sender: other,
isOutgoing: false) isOutgoing: false)
XCTAssertEqual(string, L10n.stateEventRoomBan(other.displayName ?? other.id, banned.displayName ?? banned.id)) #expect(string == L10n.stateEventRoomBan(other.displayName ?? other.id, banned.displayName ?? banned.id))
} }
} }

View File

@@ -9,100 +9,94 @@
@testable import ElementX @testable import ElementX
import MatrixRustSDK import MatrixRustSDK
import MatrixRustSDKMocks 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 appSettings: AppSettings!
var roomList: RoomListSDKMock! var roomList: RoomListSDKMock!
var dynamicEntriesController: RoomListDynamicEntriesControllerSDKMock! var dynamicEntriesController: RoomListDynamicEntriesControllerSDKMock!
let baseFilters: [RoomListEntriesDynamicFilterKind] = [.any(filters: [.all(filters: [.nonSpace, .nonLeft]),
.all(filters: [.space, .invite])]),
.deduplicateVersions]
var roomSummaryProvider: RoomSummaryProvider! var roomSummaryProvider: RoomSummaryProvider!
override func setUp() { deinit {
AppSettings.resetAllSettings()
appSettings = AppSettings()
}
override func tearDown() {
AppSettings.resetAllSettings() AppSettings.resetAllSettings()
} }
func testDefaultRustFilters() async { @Test
func defaultRustFilters() async {
// Given a new room provider. // Given a new room provider.
setupProvider() setup()
await Task.yield() await Task.yield()
// Then it should have the default Rust filters enabled. // Then it should have the default Rust filters enabled.
XCTAssertEqual(dynamicEntriesController.setFilterKindCallsCount, 1) #expect(dynamicEntriesController.setFilterKindCallsCount == 1)
XCTAssertEqual(dynamicEntriesController.setFilterKindReceivedInvocations.last, #expect(dynamicEntriesController.setFilterKindReceivedInvocations.last == .all(filters: baseFilters))
.all(filters: baseFilters))
// When setting one our user filters. // When setting one our user filters.
roomSummaryProvider.setFilter(.all(filters: [.favourites])) roomSummaryProvider.setFilter(.all(filters: [.favourites]))
await Task.yield() await Task.yield()
// Then that filter should be added to the default Rust filters. // Then that filter should be added to the default Rust filters.
XCTAssertEqual(dynamicEntriesController.setFilterKindCallsCount, 2) #expect(dynamicEntriesController.setFilterKindCallsCount == 2)
XCTAssertEqual(dynamicEntriesController.setFilterKindReceivedInvocations.last, #expect(dynamicEntriesController.setFilterKindReceivedInvocations.last == .all(filters: [.all(filters: [.favourite, .joined])] + baseFilters))
.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. // Given a new room provider with the low priority filter enabled.
setupProvider(isLowPriorityFilterEnabled: true) setup(isLowPriorityFilterEnabled: true)
await Task.yield() await Task.yield()
// Then the default Rust filters should include the non-low priority filter, // 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. // so that low priority rooms are hidden from the top of the room list.
XCTAssertEqual(dynamicEntriesController.setFilterKindCallsCount, 1) #expect(dynamicEntriesController.setFilterKindCallsCount == 1)
XCTAssertEqual(dynamicEntriesController.setFilterKindReceivedInvocations.last, #expect(dynamicEntriesController.setFilterKindReceivedInvocations.last == .all(filters: baseFilters + [.nonLowPriority]))
.all(filters: baseFilters + [.nonLowPriority]))
// When setting the low priority filter. // When setting the low priority filter.
roomSummaryProvider.setFilter(.all(filters: [.lowPriority])) roomSummaryProvider.setFilter(.all(filters: [.lowPriority]))
await Task.yield() await Task.yield()
// Then the non-low priority filter should be replaced with the low priority filter. // Then the non-low priority filter should be replaced with the low priority filter.
XCTAssertEqual(dynamicEntriesController.setFilterKindCallsCount, 2) #expect(dynamicEntriesController.setFilterKindCallsCount == 2)
XCTAssertEqual(dynamicEntriesController.setFilterKindReceivedInvocations.last, #expect(dynamicEntriesController.setFilterKindReceivedInvocations.last == .all(filters: [.all(filters: [.lowPriority, .joined])] + baseFilters))
.all(filters: [.all(filters: [.lowPriority, .joined])] + baseFilters))
// When setting another one of our filters. // When setting another one of our filters.
roomSummaryProvider.setFilter(.all(filters: [.rooms])) roomSummaryProvider.setFilter(.all(filters: [.rooms]))
await Task.yield() await Task.yield()
// Then the filter should be combined with the non-low priority filter. // Then the filter should be combined with the non-low priority filter.
XCTAssertEqual(dynamicEntriesController.setFilterKindCallsCount, 3) #expect(dynamicEntriesController.setFilterKindCallsCount == 3)
XCTAssertEqual(dynamicEntriesController.setFilterKindReceivedInvocations.last, #expect(dynamicEntriesController.setFilterKindReceivedInvocations.last == .all(filters: [.all(filters: [.category(expect: .group), .joined])] + baseFilters + [.nonLowPriority]))
.all(filters: [.all(filters: [.category(expect: .group), .joined])] + baseFilters + [.nonLowPriority]))
} }
func testRoomIdentifierFilters() async { @Test
setupProvider() func roomIdentifierFilters() async {
setup()
await Task.yield() await Task.yield()
// Then it should have the default Rust filters enabled. // Then it should have the default Rust filters enabled.
XCTAssertEqual(dynamicEntriesController.setFilterKindCallsCount, 1) #expect(dynamicEntriesController.setFilterKindCallsCount == 1)
XCTAssertEqual(dynamicEntriesController.setFilterKindReceivedInvocations.last, #expect(dynamicEntriesController.setFilterKindReceivedInvocations.last == .all(filters: baseFilters))
.all(filters: baseFilters))
// When setting one our user filters. // When setting one our user filters.
roomSummaryProvider.setFilter(.rooms(roomsIDs: ["SomeRoom"], filters: [.favourites])) roomSummaryProvider.setFilter(.rooms(roomsIDs: ["SomeRoom"], filters: [.favourites]))
await Task.yield() await Task.yield()
// Then that filter should be added to the default Rust filters. // Then that filter should be added to the default Rust filters.
XCTAssertEqual(dynamicEntriesController.setFilterKindCallsCount, 2) #expect(dynamicEntriesController.setFilterKindCallsCount == 2)
XCTAssertEqual(dynamicEntriesController.setFilterKindReceivedInvocations.last, #expect(dynamicEntriesController.setFilterKindReceivedInvocations.last == .all(filters: [.all(filters: [.favourite, .joined])] + baseFilters + [.identifiers(identifiers: ["SomeRoom"])]))
.all(filters: [.all(filters: [.favourite, .joined])] + baseFilters + [.identifiers(identifiers: ["SomeRoom"])]))
} }
// MARK: - Helpers // MARK: - Helpers
private func setupProvider(isLowPriorityFilterEnabled: Bool = false) { private func setup(isLowPriorityFilterEnabled: Bool = false) {
AppSettings.resetAllSettings()
appSettings = AppSettings()
appSettings.lowPriorityFilterEnabled = isLowPriorityFilterEnabled appSettings.lowPriorityFilterEnabled = isLowPriorityFilterEnabled
let stateEventStringBuilder = RoomStateEventStringBuilder(userID: "@me:matrix.org") let stateEventStringBuilder = RoomStateEventStringBuilder(userID: "@me:matrix.org")

View File

@@ -7,83 +7,90 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Foundation
import Testing
class RoomSummaryTests: XCTestCase { @Suite
struct RoomSummaryTests {
// swiftlint:disable:next large_tuple // swiftlint:disable:next large_tuple
let roomDetails: (id: String, name: String, avatarURL: URL) = ("room_id", "Room Name", "mxc://hs.tld/room/avatar") 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")] 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) let details = makeSummary(isDirect: false, isSpace: false, hasRoomAvatar: true, isTombstoned: false)
switch details.avatar { switch details.avatar {
case .room(let id, let name, let avatarURL): case .room(let id, let name, let avatarURL):
XCTAssertEqual(id, roomDetails.id) #expect(id == roomDetails.id)
XCTAssertEqual(name, roomDetails.name) #expect(name == roomDetails.name)
XCTAssertEqual(avatarURL, roomDetails.avatarURL) #expect(avatarURL == roomDetails.avatarURL)
case .heroes: 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: case .space:
XCTFail("A room shouldn't use a space avatar.") Issue.record("A room shouldn't use a space avatar.")
case .tombstoned: 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) let details = makeSummary(isDirect: true, isSpace: false, hasRoomAvatar: true, isTombstoned: false)
switch details.avatar { switch details.avatar {
case .room(let id, let name, let avatarURL): case .room(let id, let name, let avatarURL):
XCTAssertEqual(id, roomDetails.id) #expect(id == roomDetails.id)
XCTAssertEqual(name, roomDetails.name) #expect(name == roomDetails.name)
XCTAssertEqual(avatarURL, roomDetails.avatarURL) #expect(avatarURL == roomDetails.avatarURL)
case .heroes: 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: case .space:
XCTFail("A DM shouldn't use a space avatar.") Issue.record("A DM shouldn't use a space avatar.")
case .tombstoned: 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) let details = makeSummary(isDirect: true, isSpace: false, hasRoomAvatar: false, isTombstoned: false)
switch details.avatar { switch details.avatar {
case .room: 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): case .heroes(let heroes):
XCTAssertEqual(heroes, self.heroes) #expect(heroes == self.heroes)
case .space: case .space:
XCTFail("A DM shouldn't use a space avatar.") Issue.record("A DM shouldn't use a space avatar.")
case .tombstoned: 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) let details = makeSummary(isDirect: false, isSpace: true, hasRoomAvatar: true, isTombstoned: false)
switch details.avatar { switch details.avatar {
case .room: case .room:
XCTFail("A space shouldn't use a room avatar.") Issue.record("A space shouldn't use a room avatar.")
case .heroes: 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): case .space(let id, let name, let avatarURL):
XCTAssertEqual(id, roomDetails.id) #expect(id == roomDetails.id)
XCTAssertEqual(name, roomDetails.name) #expect(name == roomDetails.name)
XCTAssertEqual(avatarURL, roomDetails.avatarURL) #expect(avatarURL == roomDetails.avatarURL)
case .tombstoned: 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) let details = makeSummary(isDirect: false, isSpace: false, hasRoomAvatar: true, isTombstoned: true)
XCTAssertEqual(details.avatar, .tombstoned) #expect(details.avatar == .tombstoned)
} }
// MARK: - Helpers // MARK: - Helpers

View File

@@ -9,27 +9,29 @@
@testable import ElementX @testable import ElementX
import MatrixRustSDK import MatrixRustSDK
import MatrixRustSDKMocks import MatrixRustSDKMocks
import XCTest import Testing
class RoomTests: XCTestCase { @Suite
func testCallIntent() async { struct RoomTests {
@Test
func callIntent() async {
let room = RoomSDKMock() let room = RoomSDKMock()
room.hasActiveRoomCallReturnValue = false room.hasActiveRoomCallReturnValue = false
room.isDirectReturnValue = false room.isDirectReturnValue = false
var callIntent = await room.joinCallIntent var callIntent = await room.joinCallIntent
XCTAssertEqual(callIntent, .startCall) #expect(callIntent == .startCall)
room.isDirectReturnValue = true room.isDirectReturnValue = true
callIntent = await room.joinCallIntent callIntent = await room.joinCallIntent
XCTAssertEqual(callIntent, .startCallDm) #expect(callIntent == .startCallDm)
room.hasActiveRoomCallReturnValue = true room.hasActiveRoomCallReturnValue = true
callIntent = await room.joinCallIntent callIntent = await room.joinCallIntent
XCTAssertEqual(callIntent, .joinExistingDm) #expect(callIntent == .joinExistingDm)
room.isDirectReturnValue = false room.isDirectReturnValue = false
callIntent = await room.joinCallIntent callIntent = await room.joinCallIntent
XCTAssertEqual(callIntent, .joinExisting) #expect(callIntent == .joinExisting)
} }
} }

View File

@@ -1,13 +0,0 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
@testable import ElementX
import XCTest
@MainActor
class SecureBackupKeyBackupScreenViewModelTests: XCTestCase { }

View File

@@ -8,19 +8,20 @@
import Combine import Combine
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class SecureBackupLogoutConfirmationScreenViewModelTests: XCTestCase { @Suite
var viewModel: SecureBackupLogoutConfirmationScreenViewModel! struct SecureBackupLogoutConfirmationScreenViewModelTests {
var context: SecureBackupLogoutConfirmationScreenViewModel.Context { private var viewModel: SecureBackupLogoutConfirmationScreenViewModel
private var context: SecureBackupLogoutConfirmationScreenViewModel.Context {
viewModel.context viewModel.context
} }
var secureBackupController: SecureBackupControllerMock! private var secureBackupController: SecureBackupControllerMock
var reachabilitySubject: CurrentValueSubject<NetworkMonitorReachability, Never>! private var reachabilitySubject: CurrentValueSubject<NetworkMonitorReachability, Never>
override func setUp() { init() {
secureBackupController = SecureBackupControllerMock() secureBackupController = SecureBackupControllerMock()
secureBackupController.underlyingKeyBackupState = CurrentValueSubject<SecureBackupKeyBackupState, Never>(.enabled).asCurrentValuePublisher() secureBackupController.underlyingKeyBackupState = CurrentValueSubject<SecureBackupKeyBackupState, Never>(.enabled).asCurrentValuePublisher()
@@ -30,36 +31,57 @@ class SecureBackupLogoutConfirmationScreenViewModelTests: XCTestCase {
homeserverReachabilityPublisher: reachabilitySubject.asCurrentValuePublisher()) homeserverReachabilityPublisher: reachabilitySubject.asCurrentValuePublisher())
} }
func testInitialState() { @Test
XCTAssertEqual(context.viewState.mode, .saveRecoveryKey) func initialState() {
#expect(context.viewState.mode == .saveRecoveryKey)
} }
func testOngoingState() async throws { @Test
testInitialState() func ongoingState() async throws {
#expect(context.viewState.mode == .saveRecoveryKey)
let progressExpectation = expectation(description: "The upload progress callback should be called.") try await confirmation { confirmation in
secureBackupController.waitForKeyBackupUploadUploadStateSubjectClosure = { stateSubject in secureBackupController.waitForKeyBackupUploadUploadStateSubjectClosure = { stateSubject in
try? await Task.sleep(for: .seconds(4)) try? await Task.sleep(for: .seconds(4))
stateSubject.send(.uploading(uploadedKeyCount: 50, totalKeyCount: 100)) stateSubject.send(.uploading(uploadedKeyCount: 50, totalKeyCount: 100))
progressExpectation.fulfill() confirmation()
return .success(()) 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 { @Test
try await testOngoingState() 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 } let deferred = deferFulfillment(context.observe(\.viewState.mode)) { $0 == .offline }
reachabilitySubject.send(.unreachable) reachabilitySubject.send(.unreachable)

View File

@@ -1,13 +0,0 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
@testable import ElementX
import XCTest
@MainActor
class SecureBackupRecoveryKeyScreenViewModelTests: XCTestCase { }

View File

@@ -1,13 +0,0 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
@testable import ElementX
import XCTest
@MainActor
class SecureBackupScreenViewModelTests: XCTestCase { }

View File

@@ -7,35 +7,38 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class ServerConfirmationScreenViewStateTests: XCTestCase { @Suite
func testLoginMessageString() { struct ServerConfirmationScreenViewStateTests {
@Test
func loginMessageString() {
let matrixDotOrgLogin = ServerConfirmationScreenViewState(mode: .confirmation(LoginHomeserver.mockMatrixDotOrg.address), let matrixDotOrgLogin = ServerConfirmationScreenViewState(mode: .confirmation(LoginHomeserver.mockMatrixDotOrg.address),
authenticationFlow: .login) 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"), let elementDotIoLogin = ServerConfirmationScreenViewState(mode: .confirmation("element.io"),
authenticationFlow: .login) 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), let otherLogin = ServerConfirmationScreenViewState(mode: .confirmation(LoginHomeserver.mockOIDC.address),
authenticationFlow: .login) 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"]), let pickerLogin = ServerConfirmationScreenViewState(mode: .picker(["element.io", "matrix.org"]),
authenticationFlow: .login) 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), let matrixDotOrgRegister = ServerConfirmationScreenViewState(mode: .confirmation(LoginHomeserver.mockMatrixDotOrg.address),
authenticationFlow: .register) 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), let oidcRegister = ServerConfirmationScreenViewState(mode: .confirmation(LoginHomeserver.mockOIDC.address),
authenticationFlow: .register) authenticationFlow: .register)
XCTAssertEqual(oidcRegister.message, L10n.screenServerConfirmationMessageRegister, "The registration message should always be the same.") #expect(oidcRegister.message == L10n.screenServerConfirmationMessageRegister, "The registration message should always be the same.")
} }
} }

View File

@@ -7,23 +7,25 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class ServerSelectionScreenViewModelTests: XCTestCase { @Suite
struct ServerSelectionScreenViewModelTests {
var clientFactory: AuthenticationClientFactoryMock! var clientFactory: AuthenticationClientFactoryMock!
var service: AuthenticationServiceProtocol! var service: AuthenticationServiceProtocol!
var viewModel: ServerSelectionScreenViewModelProtocol! var viewModel: ServerSelectionScreenViewModelProtocol!
var context: ServerSelectionScreenViewModelType.Context { var context: ServerSelectionScreenViewModelType.Context {
viewModel.context viewModel.context
} }
func testSelectForLogin() async throws { @Test
mutating func selectForLogin() async throws {
// Given a view model for login. // Given a view model for login.
setupViewModel(authenticationFlow: .login) setup(authenticationFlow: .login)
XCTAssertEqual(service.homeserver.value.loginMode, .unknown) #expect(service.homeserver.value.loginMode == .unknown)
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0) #expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
// When selecting matrix.org. // When selecting matrix.org.
context.homeserverAddress = "matrix.org" context.homeserverAddress = "matrix.org"
@@ -32,16 +34,17 @@ class ServerSelectionScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
// Then selection should succeed. // Then selection should succeed.
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) #expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
XCTAssertEqual(service.homeserver.value, .mockMatrixDotOrg) #expect(service.homeserver.value == .mockMatrixDotOrg)
} }
func testLoginNotSupportedAlert() async throws { @Test
mutating func loginNotSupportedAlert() async throws {
// Given a view model for login. // Given a view model for login.
setupViewModel(authenticationFlow: .login) setup(authenticationFlow: .login)
XCTAssertEqual(service.homeserver.value.loginMode, .unknown) #expect(service.homeserver.value.loginMode == .unknown)
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0) #expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
XCTAssertNil(context.alertInfo) #expect(context.alertInfo == nil)
// When selecting a server that doesn't support login. // When selecting a server that doesn't support login.
context.homeserverAddress = "server.net" context.homeserverAddress = "server.net"
@@ -50,15 +53,16 @@ class ServerSelectionScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
// Then selection should fail with an alert about not supporting registration. // Then selection should fail with an alert about not supporting registration.
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) #expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
XCTAssertEqual(context.alertInfo?.id, .loginAlert) #expect(context.alertInfo?.id == .loginAlert)
} }
func testSelectForRegistration() async throws { @Test
mutating func selectForRegistration() async throws {
// Given a view model for registration. // Given a view model for registration.
setupViewModel(authenticationFlow: .register) setup(authenticationFlow: .register)
XCTAssertEqual(service.homeserver.value.loginMode, .unknown) #expect(service.homeserver.value.loginMode == .unknown)
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0) #expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
// When selecting matrix.org. // When selecting matrix.org.
context.homeserverAddress = "matrix.org" context.homeserverAddress = "matrix.org"
@@ -67,16 +71,17 @@ class ServerSelectionScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
// Then selection should succeed. // Then selection should succeed.
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) #expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
XCTAssertEqual(service.homeserver.value, .mockMatrixDotOrg) #expect(service.homeserver.value == .mockMatrixDotOrg)
} }
func testRegistrationNotSupportedAlert() async throws { @Test
mutating func registrationNotSupportedAlert() async throws {
// Given a view model for registration. // Given a view model for registration.
setupViewModel(authenticationFlow: .register) setup(authenticationFlow: .register)
XCTAssertEqual(service.homeserver.value.loginMode, .unknown) #expect(service.homeserver.value.loginMode == .unknown)
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0) #expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
XCTAssertNil(context.alertInfo) #expect(context.alertInfo == nil)
// When selecting a server that doesn't support registration. // When selecting a server that doesn't support registration.
context.homeserverAddress = "example.com" context.homeserverAddress = "example.com"
@@ -85,16 +90,17 @@ class ServerSelectionScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
// Then selection should fail with an alert about not supporting registration. // Then selection should fail with an alert about not supporting registration.
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) #expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
XCTAssertEqual(context.alertInfo?.id, .registrationAlert) #expect(context.alertInfo?.id == .registrationAlert)
} }
func testElementProRequiredAlert() async throws { @Test
mutating func elementProRequiredAlert() async throws {
// Given a view model for login. // Given a view model for login.
setupViewModel(authenticationFlow: .login) setup(authenticationFlow: .login)
XCTAssertEqual(service.homeserver.value.loginMode, .unknown) #expect(service.homeserver.value.loginMode == .unknown)
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0) #expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
XCTAssertNil(context.alertInfo) #expect(context.alertInfo == nil)
// When selecting a server that requires Element Pro // When selecting a server that requires Element Pro
context.homeserverAddress = "secure.gov" context.homeserverAddress = "secure.gov"
@@ -103,17 +109,18 @@ class ServerSelectionScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
// Then selection should fail with an alert telling the user to download Element Pro. // Then selection should fail with an alert telling the user to download Element Pro.
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) #expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
XCTAssertEqual(context.alertInfo?.id, .elementProAlert) #expect(context.alertInfo?.id == .elementProAlert)
} }
func testInvalidServer() async throws { @Test
mutating func invalidServer() async throws {
// Given a new instance of the view model. // Given a new instance of the view model.
setupViewModel(authenticationFlow: .login) setup(authenticationFlow: .login)
XCTAssertFalse(context.viewState.isShowingFooterError, "There should not be an error message for a new view model.") #expect(!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.") #expect(context.viewState.footerErrorMessage == nil, "There should not be an error message for a new view model.")
XCTAssertEqual(String(context.viewState.footerMessage), L10n.screenChangeServerFormNotice, #expect(String(context.viewState.footerMessage) == L10n.screenChangeServerFormNotice,
"The standard footer message should be shown.") "The standard footer message should be shown.")
// When attempting to discover an invalid server // When attempting to discover an invalid server
var deferred = deferFulfillment(context.observe(\.viewState.isShowingFooterError)) { $0 } var deferred = deferFulfillment(context.observe(\.viewState.isShowingFooterError)) { $0 }
@@ -122,10 +129,10 @@ class ServerSelectionScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
// Then the footer should now be showing an error. // Then the footer should now be showing an error.
XCTAssertTrue(context.viewState.isShowingFooterError, "The error message should be stored.") #expect(context.viewState.isShowingFooterError, "The error message should be stored.")
XCTAssertNotNil(context.viewState.footerErrorMessage, "The error message should be stored.") #expect(context.viewState.footerErrorMessage != nil, "The error message should be stored.")
XCTAssertNotEqual(String(context.viewState.footerMessage), L10n.screenChangeServerFormNotice, #expect(String(context.viewState.footerMessage) != L10n.screenChangeServerFormNotice,
"The error message should be shown.") "The error message should be shown.")
// And when clearing the error. // And when clearing the error.
deferred = deferFulfillment(context.observe(\.viewState.isShowingFooterError)) { !$0 } deferred = deferFulfillment(context.observe(\.viewState.isShowingFooterError)) { !$0 }
@@ -134,14 +141,14 @@ class ServerSelectionScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
// Then the error message should now be removed. // Then the error message should now be removed.
XCTAssertNil(context.viewState.footerErrorMessage, "The error message should have been cleared.") #expect(context.viewState.footerErrorMessage == nil, "The error message should have been cleared.")
XCTAssertEqual(String(context.viewState.footerMessage), L10n.screenChangeServerFormNotice, #expect(String(context.viewState.footerMessage) == L10n.screenChangeServerFormNotice,
"The standard footer message should be shown again.") "The standard footer message should be shown again.")
} }
// MARK: - Helpers // MARK: - Helpers
private func setupViewModel(authenticationFlow: AuthenticationFlow) { private mutating func setup(authenticationFlow: AuthenticationFlow) {
clientFactory = AuthenticationClientFactoryMock(configuration: .init()) clientFactory = AuthenticationClientFactoryMock(configuration: .init())
service = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()), service = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
encryptionKeyProvider: EncryptionKeyProvider(), encryptionKeyProvider: EncryptionKeyProvider(),

View File

@@ -7,12 +7,15 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Foundation
import Testing
class SessionDirectoriesTests: XCTestCase { @Suite
struct SessionDirectoriesTests {
let fileManager = FileManager.default let fileManager = FileManager.default
func testInitWithDataDirectory() { @Test
func initWithDataDirectory() {
// Given only a session directory without a caches directory. // Given only a session directory without a caches directory.
let sessionDirectoryName = UUID().uuidString let sessionDirectoryName = UUID().uuidString
let sessionDirectory = URL.applicationSupportBaseDirectory.appending(component: sessionDirectoryName) let sessionDirectory = URL.applicationSupportBaseDirectory.appending(component: sessionDirectoryName)
@@ -21,11 +24,12 @@ class SessionDirectoriesTests: XCTestCase {
let sessionDirectories = SessionDirectories(dataDirectory: sessionDirectory) let sessionDirectories = SessionDirectories(dataDirectory: sessionDirectory)
// Then the data directory should remain unchanged and the caches directory should be generated. // Then the data directory should remain unchanged and the caches directory should be generated.
XCTAssertEqual(sessionDirectories.dataDirectory, sessionDirectory) #expect(sessionDirectories.dataDirectory == sessionDirectory)
XCTAssertEqual(sessionDirectories.cacheDirectory, .sessionCachesBaseDirectory.appending(component: sessionDirectoryName)) #expect(sessionDirectories.cacheDirectory == .sessionCachesBaseDirectory.appending(component: sessionDirectoryName))
} }
func testPathOutput() { @Test
func pathOutput() {
// Given session directories created from paths with spaces in them. // Given session directories created from paths with spaces in them.
let originalDataPath = "/Users/John Smith/Data" let originalDataPath = "/Users/John Smith/Data"
let originalCachePath = "/Users/John Smith/Caches" let originalCachePath = "/Users/John Smith/Caches"
@@ -38,53 +42,55 @@ class SessionDirectoriesTests: XCTestCase {
let returnedCachePath = sessionDirectories.cachePath let returnedCachePath = sessionDirectories.cachePath
// Then the paths should not be escaped. // Then the paths should not be escaped.
XCTAssertEqual(returnedDataPath, originalDataPath) #expect(returnedDataPath == originalDataPath)
XCTAssertEqual(returnedCachePath, originalCachePath) #expect(returnedCachePath == originalCachePath)
} }
func testDeleteDirectories() throws { @Test
func deleteDirectories() throws {
// Given a new set of session directories. // Given a new set of session directories.
let sessionDirectories = SessionDirectories() let sessionDirectories = SessionDirectories()
try fileManager.createDirectory(at: sessionDirectories.dataDirectory, withIntermediateDirectories: true) try fileManager.createDirectory(at: sessionDirectories.dataDirectory, withIntermediateDirectories: true)
try fileManager.createDirectory(at: sessionDirectories.cacheDirectory, withIntermediateDirectories: true) try fileManager.createDirectory(at: sessionDirectories.cacheDirectory, withIntermediateDirectories: true)
XCTAssertTrue(fileManager.directoryExists(at: sessionDirectories.dataDirectory)) #expect(fileManager.directoryExists(at: sessionDirectories.dataDirectory))
XCTAssertTrue(fileManager.directoryExists(at: sessionDirectories.cacheDirectory)) #expect(fileManager.directoryExists(at: sessionDirectories.cacheDirectory))
// When deleting the directories. // When deleting the directories.
sessionDirectories.delete() sessionDirectories.delete()
// Then neither directory should exist on disk. // Then neither directory should exist on disk.
XCTAssertFalse(fileManager.directoryExists(at: sessionDirectories.dataDirectory)) #expect(!fileManager.directoryExists(at: sessionDirectories.dataDirectory))
XCTAssertFalse(fileManager.directoryExists(at: sessionDirectories.cacheDirectory)) #expect(!fileManager.directoryExists(at: sessionDirectories.cacheDirectory))
} }
func testDeleteTransientUserData() throws { @Test
func deleteTransientUserData() throws {
// Given a set of session directories with some databases. // Given a set of session directories with some databases.
let sessionDirectories = SessionDirectories() let sessionDirectories = SessionDirectories()
try fileManager.createDirectory(at: sessionDirectories.dataDirectory, withIntermediateDirectories: true) try fileManager.createDirectory(at: sessionDirectories.dataDirectory, withIntermediateDirectories: true)
try fileManager.createDirectory(at: sessionDirectories.cacheDirectory, withIntermediateDirectories: true) try fileManager.createDirectory(at: sessionDirectories.cacheDirectory, withIntermediateDirectories: true)
XCTAssertTrue(fileManager.directoryExists(at: sessionDirectories.dataDirectory)) #expect(fileManager.directoryExists(at: sessionDirectories.dataDirectory))
XCTAssertTrue(fileManager.directoryExists(at: sessionDirectories.cacheDirectory)) #expect(fileManager.directoryExists(at: sessionDirectories.cacheDirectory))
sessionDirectories.generateMockData() sessionDirectories.generateMockData()
XCTAssertTrue(fileManager.fileExists(atPath: sessionDirectories.mockStateStorePath)) #expect(fileManager.fileExists(atPath: sessionDirectories.mockStateStorePath))
XCTAssertTrue(fileManager.fileExists(atPath: sessionDirectories.mockCryptoStorePath)) #expect(fileManager.fileExists(atPath: sessionDirectories.mockCryptoStorePath))
XCTAssertTrue(fileManager.fileExists(atPath: sessionDirectories.mockEventCachePath)) #expect(fileManager.fileExists(atPath: sessionDirectories.mockEventCachePath))
XCTAssertEqual(try fileManager.numberOfItems(at: sessionDirectories.dataDirectory), 6) #expect(try fileManager.numberOfItems(at: sessionDirectories.dataDirectory) == 6)
XCTAssertEqual(try fileManager.numberOfItems(at: sessionDirectories.cacheDirectory), 3) #expect(try fileManager.numberOfItems(at: sessionDirectories.cacheDirectory) == 3)
// When deleting transient user data. // When deleting transient user data.
sessionDirectories.deleteTransientUserData() sessionDirectories.deleteTransientUserData()
// Then the data directory should only contain the crypto store and the cache directory should remain but be empty. // 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)) #expect(fileManager.directoryExists(at: sessionDirectories.dataDirectory))
XCTAssertEqual(try fileManager.numberOfItems(at: sessionDirectories.dataDirectory), 3) #expect(try fileManager.numberOfItems(at: sessionDirectories.dataDirectory) == 3)
XCTAssertFalse(fileManager.fileExists(atPath: sessionDirectories.mockStateStorePath)) #expect(!fileManager.fileExists(atPath: sessionDirectories.mockStateStorePath))
XCTAssertTrue(fileManager.fileExists(atPath: sessionDirectories.mockCryptoStorePath)) #expect(fileManager.fileExists(atPath: sessionDirectories.mockCryptoStorePath))
XCTAssertTrue(fileManager.directoryExists(at: sessionDirectories.cacheDirectory)) #expect(fileManager.directoryExists(at: sessionDirectories.cacheDirectory))
XCTAssertEqual(try fileManager.numberOfItems(at: sessionDirectories.cacheDirectory), 0) #expect(try fileManager.numberOfItems(at: sessionDirectories.cacheDirectory) == 0)
XCTAssertFalse(fileManager.fileExists(atPath: sessionDirectories.mockEventCachePath)) #expect(!fileManager.fileExists(atPath: sessionDirectories.mockEventCachePath))
// The tests are done, tidy up these useless directories 🧹 // The tests are done, tidy up these useless directories 🧹
sessionDirectories.delete() sessionDirectories.delete()

View File

@@ -7,105 +7,108 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class SessionVerificationStateMachineTests: XCTestCase { @Suite
private var stateMachine: SessionVerificationScreenStateMachine! struct SessionVerificationStateMachineTests {
private var stateMachine: SessionVerificationScreenStateMachine
@MainActor init() {
override func setUpWithError() throws {
stateMachine = SessionVerificationScreenStateMachine(state: .initial) stateMachine = SessionVerificationScreenStateMachine(state: .initial)
} }
func testAcceptChallenge() { @Test
XCTAssertEqual(stateMachine.state, .initial) func acceptChallenge() {
#expect(stateMachine.state == .initial)
stateMachine.processEvent(.requestVerification) stateMachine.processEvent(.requestVerification)
XCTAssertEqual(stateMachine.state, .requestingVerification) #expect(stateMachine.state == .requestingVerification)
stateMachine.processEvent(.didAcceptVerificationRequest) stateMachine.processEvent(.didAcceptVerificationRequest)
XCTAssertEqual(stateMachine.state, .verificationRequestAccepted) #expect(stateMachine.state == .verificationRequestAccepted)
stateMachine.processEvent(.didStartSasVerification) stateMachine.processEvent(.didStartSasVerification)
XCTAssertEqual(stateMachine.state, .sasVerificationStarted) #expect(stateMachine.state == .sasVerificationStarted)
stateMachine.processEvent(.didReceiveChallenge(emojis: SessionVerificationControllerProxyMock.emojis)) stateMachine.processEvent(.didReceiveChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
XCTAssertEqual(stateMachine.state, .showingChallenge(emojis: SessionVerificationControllerProxyMock.emojis)) #expect(stateMachine.state == .showingChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
stateMachine.processEvent(.acceptChallenge) stateMachine.processEvent(.acceptChallenge)
XCTAssertEqual(stateMachine.state, .acceptingChallenge(emojis: SessionVerificationControllerProxyMock.emojis)) #expect(stateMachine.state == .acceptingChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
stateMachine.processEvent(.didAcceptChallenge) stateMachine.processEvent(.didAcceptChallenge)
XCTAssertEqual(stateMachine.state, .verified) #expect(stateMachine.state == .verified)
} }
func testDeclineChallenge() { @Test
XCTAssertEqual(stateMachine.state, .initial) func declineChallenge() {
#expect(stateMachine.state == .initial)
stateMachine.processEvent(.requestVerification) stateMachine.processEvent(.requestVerification)
XCTAssertEqual(stateMachine.state, .requestingVerification) #expect(stateMachine.state == .requestingVerification)
stateMachine.processEvent(.didAcceptVerificationRequest) stateMachine.processEvent(.didAcceptVerificationRequest)
XCTAssertEqual(stateMachine.state, .verificationRequestAccepted) #expect(stateMachine.state == .verificationRequestAccepted)
stateMachine.processEvent(.didStartSasVerification) stateMachine.processEvent(.didStartSasVerification)
XCTAssertEqual(stateMachine.state, .sasVerificationStarted) #expect(stateMachine.state == .sasVerificationStarted)
stateMachine.processEvent(.didReceiveChallenge(emojis: SessionVerificationControllerProxyMock.emojis)) stateMachine.processEvent(.didReceiveChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
XCTAssertEqual(stateMachine.state, .showingChallenge(emojis: SessionVerificationControllerProxyMock.emojis)) #expect(stateMachine.state == .showingChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
stateMachine.processEvent(.declineChallenge) stateMachine.processEvent(.declineChallenge)
XCTAssertEqual(stateMachine.state, .decliningChallenge(emojis: SessionVerificationControllerProxyMock.emojis)) #expect(stateMachine.state == .decliningChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
stateMachine.processEvent(.didCancel) stateMachine.processEvent(.didCancel)
XCTAssertEqual(stateMachine.state, .cancelled) #expect(stateMachine.state == .cancelled)
stateMachine.processEvent(.restart) stateMachine.processEvent(.restart)
XCTAssertEqual(stateMachine.state, .initial) #expect(stateMachine.state == .initial)
} }
func testCancellation() { @Test
XCTAssertEqual(stateMachine.state, .initial) func cancellation() {
#expect(stateMachine.state == .initial)
stateMachine.processEvent(.requestVerification) stateMachine.processEvent(.requestVerification)
XCTAssertEqual(stateMachine.state, .requestingVerification) #expect(stateMachine.state == .requestingVerification)
stateMachine.processEvent(.cancel) stateMachine.processEvent(.cancel)
XCTAssertEqual(stateMachine.state, .cancelling) #expect(stateMachine.state == .cancelling)
stateMachine.processEvent(.didCancel) stateMachine.processEvent(.didCancel)
XCTAssertEqual(stateMachine.state, .cancelled) #expect(stateMachine.state == .cancelled)
// This duplication is intentional // This duplication is intentional
stateMachine.processEvent(.didCancel) stateMachine.processEvent(.didCancel)
XCTAssertEqual(stateMachine.state, .cancelled) #expect(stateMachine.state == .cancelled)
stateMachine.processEvent(.restart) stateMachine.processEvent(.restart)
XCTAssertEqual(stateMachine.state, .initial) #expect(stateMachine.state == .initial)
stateMachine.processEvent(.requestVerification) stateMachine.processEvent(.requestVerification)
XCTAssertEqual(stateMachine.state, .requestingVerification) #expect(stateMachine.state == .requestingVerification)
stateMachine.processEvent(.didAcceptVerificationRequest) stateMachine.processEvent(.didAcceptVerificationRequest)
XCTAssertEqual(stateMachine.state, .verificationRequestAccepted) #expect(stateMachine.state == .verificationRequestAccepted)
stateMachine.processEvent(.didStartSasVerification) stateMachine.processEvent(.didStartSasVerification)
XCTAssertEqual(stateMachine.state, .sasVerificationStarted) #expect(stateMachine.state == .sasVerificationStarted)
stateMachine.processEvent(.didReceiveChallenge(emojis: SessionVerificationControllerProxyMock.emojis)) stateMachine.processEvent(.didReceiveChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
XCTAssertEqual(stateMachine.state, .showingChallenge(emojis: SessionVerificationControllerProxyMock.emojis)) #expect(stateMachine.state == .showingChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
stateMachine.processEvent(.cancel) stateMachine.processEvent(.cancel)
XCTAssertEqual(stateMachine.state, .cancelling) #expect(stateMachine.state == .cancelling)
stateMachine.processEvent(.didCancel) stateMachine.processEvent(.didCancel)
XCTAssertEqual(stateMachine.state, .cancelled) #expect(stateMachine.state == .cancelled)
stateMachine.processEvent(.restart) stateMachine.processEvent(.restart)
XCTAssertEqual(stateMachine.state, .initial) #expect(stateMachine.state == .initial)
stateMachine.processEvent(.restart) stateMachine.processEvent(.restart)
XCTAssertEqual(stateMachine.state, .initial) #expect(stateMachine.state == .initial)
} }
} }

View File

@@ -8,16 +8,15 @@
import Combine import Combine
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class SettingsScreenViewModelTests: XCTestCase { @Suite
var viewModel: SettingsScreenViewModelProtocol! struct SettingsScreenViewModelTests {
var context: SettingsScreenViewModelType.Context! private var viewModel: SettingsScreenViewModelProtocol
var cancellables = Set<AnyCancellable>() private var context: SettingsScreenViewModelType.Context
@MainActor override func setUpWithError() throws { init() {
cancellables.removeAll()
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "")))) let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: ""))))
viewModel = SettingsScreenViewModel(userSession: userSession, viewModel = SettingsScreenViewModel(userSession: userSession,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
@@ -25,19 +24,22 @@ class SettingsScreenViewModelTests: XCTestCase {
context = viewModel.context context = viewModel.context
} }
@MainActor func testLogout() async throws { @Test
func logout() async throws {
let deferred = deferFulfillment(viewModel.actions) { $0 == .logout } let deferred = deferFulfillment(viewModel.actions) { $0 == .logout }
context.send(viewAction: .logout) context.send(viewAction: .logout)
try await deferred.fulfill() try await deferred.fulfill()
} }
func testReportBug() async throws { @Test
func reportBug() async throws {
let deferred = deferFulfillment(viewModel.actions) { $0 == .reportBug } let deferred = deferFulfillment(viewModel.actions) { $0 == .reportBug }
context.send(viewAction: .reportBug) context.send(viewAction: .reportBug)
try await deferred.fulfill() try await deferred.fulfill()
} }
func testAnalytics() async throws { @Test
func analytics() async throws {
let deferred = deferFulfillment(viewModel.actions) { $0 == .analytics } let deferred = deferFulfillment(viewModel.actions) { $0 == .analytics }
context.send(viewAction: .analytics) context.send(viewAction: .analytics)
try await deferred.fulfill() try await deferred.fulfill()

View File

@@ -7,29 +7,32 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class SoftLogoutScreenViewModelTests: XCTestCase { @Suite
let credentials = SoftLogoutScreenCredentials(userID: "mock_user_id", struct SoftLogoutScreenViewModelTests {
homeserverName: "https://example.com", private let credentials = SoftLogoutScreenCredentials(userID: "mock_user_id",
userDisplayName: "mock_username", homeserverName: "https://example.com",
deviceID: "ABCDEFGH") userDisplayName: "mock_username",
deviceID: "ABCDEFGH")
func testInitialStateForBasicServer() { @Test
func initialStateForBasicServer() {
let viewModel = SoftLogoutScreenViewModel(credentials: credentials, let viewModel = SoftLogoutScreenViewModel(credentials: credentials,
homeserver: .mockBasicServer, homeserver: .mockBasicServer,
keyBackupNeeded: false) keyBackupNeeded: false)
let context = viewModel.context let context = viewModel.context
// Given a view model where the user hasn't yet sent the verification email. // 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.") #expect(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.") #expect(!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.") #expect(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.viewState.showRecoverEncryptionKeysMessage, "The view model should not show recover encryption keys message.")
} }
func testInitialStateForBasicServerPasswordEntered() { @Test
func initialStateForBasicServerPasswordEntered() {
let viewModel = SoftLogoutScreenViewModel(credentials: credentials, let viewModel = SoftLogoutScreenViewModel(credentials: credentials,
homeserver: .mockBasicServer, homeserver: .mockBasicServer,
keyBackupNeeded: true, keyBackupNeeded: true,
@@ -37,34 +40,36 @@ class SoftLogoutScreenViewModelTests: XCTestCase {
let context = viewModel.context let context = viewModel.context
// Given a view model where the user hasn't yet sent the verification email. // 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.") #expect(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.") #expect(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.showRecoverEncryptionKeysMessage, "The view model should show recover encryption keys message.")
} }
func testInitialStateForOIDC() { @Test
func initialStateForOIDC() {
let viewModel = SoftLogoutScreenViewModel(credentials: credentials, let viewModel = SoftLogoutScreenViewModel(credentials: credentials,
homeserver: .mockMatrixDotOrg, homeserver: .mockMatrixDotOrg,
keyBackupNeeded: false) keyBackupNeeded: false)
let context = viewModel.context let context = viewModel.context
// Given a view model where the user hasn't yet sent the verification email. // 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.") #expect(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.") #expect(!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.") #expect(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.viewState.showRecoverEncryptionKeysMessage, "The view model should not show recover encryption keys message.")
} }
func testInitialStateForUnsupported() { @Test
func initialStateForUnsupported() {
let viewModel = SoftLogoutScreenViewModel(credentials: credentials, let viewModel = SoftLogoutScreenViewModel(credentials: credentials,
homeserver: .mockUnsupported, homeserver: .mockUnsupported,
keyBackupNeeded: false) keyBackupNeeded: false)
let context = viewModel.context let context = viewModel.context
// Given a view model where the user hasn't yet sent the verification email. // 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.") #expect(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.") #expect(!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.") #expect(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.viewState.showRecoverEncryptionKeysMessage, "The view model should not show recover encryption keys message.")
} }
} }

View File

@@ -8,21 +8,35 @@
import Combine import Combine
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class SpaceAddRoomsScreenViewModelTests: XCTestCase { @Suite
var spaceRoomListProxy: SpaceRoomListProxyMock! struct SpaceAddRoomsScreenViewModelTests {
var spaceServiceProxy: SpaceServiceProxyMock! var spaceRoomListProxy: SpaceRoomListProxyMock
var spaceServiceProxy: SpaceServiceProxyMock
var viewModel: SpaceAddRoomsScreenViewModelProtocol
var viewModel: SpaceAddRoomsScreenViewModelProtocol!
var context: SpaceAddRoomsScreenViewModelType.Context { var context: SpaceAddRoomsScreenViewModelType.Context {
viewModel.context viewModel.context
} }
func testAddingChildRoom() async throws { init() {
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 ?? 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), var deferred = deferFulfillment(context.observe(\.viewState.roomsSection),
message: "The screen should start with some suggestions.") { section in message: "The screen should start with some suggestions.") { section in
section.type == .suggestions && !section.rooms.isEmpty section.type == .suggestions && !section.rooms.isEmpty
@@ -37,23 +51,22 @@ class SpaceAddRoomsScreenViewModelTests: XCTestCase {
context.send(viewAction: .searchQueryChanged) context.send(viewAction: .searchQueryChanged)
try await deferred.fulfill() 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)) 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 } let deferredAction = deferFulfillment(viewModel.actions) { $0 == .dismiss }
context.send(viewAction: .save) context.send(viewAction: .save)
try await deferredAction.fulfill() try await deferredAction.fulfill()
XCTAssertTrue(spaceServiceProxy.addChildToCalled, "The room should have been added to the space.") #expect(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(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. // Given a view model with 4 selected rooms.
setupViewModel()
var deferred = deferFulfillment(context.observe(\.viewState.roomsSection), var deferred = deferFulfillment(context.observe(\.viewState.roomsSection),
message: "There should be 4 search results.") { section in message: "There should be 4 search results.") { section in
section.type == .searchResults && section.rooms.count == 4 section.type == .searchResults && section.rooms.count == 4
@@ -65,7 +78,7 @@ class SpaceAddRoomsScreenViewModelTests: XCTestCase {
for room in context.viewState.roomsSection.rooms { for room in context.viewState.roomsSection.rooms {
context.send(viewAction: .toggleRoom(room)) 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. // When there's a failure half way through saving.
let successfulIDs = context.viewState.roomsSection.rooms.map(\.id).prefix(2) let successfulIDs = context.viewState.roomsSection.rooms.map(\.id).prefix(2)
@@ -85,24 +98,10 @@ class SpaceAddRoomsScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
// Then the screen should be updated to only show the rooms that still need to be added. // 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.") #expect(spaceServiceProxy.addChildToCallsCount == 3, "The remaining calls to the service should stop after a failure.")
XCTAssertFalse(context.viewState.selectedRooms.contains { successfulIDs.contains($0.id) }, #expect(!context.viewState.selectedRooms.contains { successfulIDs.contains($0.id) },
"The added rooms should no longer show as selected.") "The added rooms should no longer show as selected.")
XCTAssertFalse(context.viewState.roomsSection.rooms.contains { successfulIDs.contains($0.id) }, #expect(!context.viewState.roomsSection.rooms.contains { successfulIDs.contains($0.id) },
"The added rooms should no longer be listed for selection.") "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())
} }
} }

View File

@@ -8,90 +8,24 @@
import Combine import Combine
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class SpacesScreenViewModelTests: XCTestCase { @Suite
var topLevelSpacesSubject: CurrentValueSubject<[SpaceServiceRoom], Never>! final class SpacesScreenViewModelTests {
var spaceServiceProxy: SpaceServiceProxyMock! var topLevelSpacesSubject: CurrentValueSubject<[SpaceServiceRoom], Never>
var appSettings: AppSettings! var spaceServiceProxy: SpaceServiceProxyMock
var appSettings: AppSettings
var viewModel: SpacesScreenViewModelProtocol! var viewModel: SpacesScreenViewModelProtocol
var context: SpacesScreenViewModelType.Context { var context: SpacesScreenViewModelType.Context {
viewModel.context viewModel.context
} }
override func setUp() { init() {
AppSettings.resetAllSettings() AppSettings.resetAllSettings()
appSettings = AppSettings() 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 clientProxy = ClientProxyMock(.init())
let userSession = UserSessionMock(.init(clientProxy: clientProxy)) let userSession = UserSessionMock(.init(clientProxy: clientProxy))
@@ -103,7 +37,7 @@ class SpacesScreenViewModelTests: XCTestCase {
spaceServiceProxy = SpaceServiceProxyMock(.init()) spaceServiceProxy = SpaceServiceProxyMock(.init())
spaceServiceProxy.topLevelSpacesPublisher = topLevelSpacesSubject.asCurrentValuePublisher() spaceServiceProxy.topLevelSpacesPublisher = topLevelSpacesSubject.asCurrentValuePublisher()
spaceServiceProxy.spaceRoomListSpaceIDClosure = { [topLevelSpacesSubject] spaceID in 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))) return .success(SpaceRoomListProxyMock(.init(spaceServiceRoom: spaceServiceRoom)))
} }
clientProxy.spaceService = spaceServiceProxy clientProxy.spaceService = spaceServiceProxy
@@ -113,4 +47,64 @@ class SpacesScreenViewModelTests: XCTestCase {
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
userIndicatorController: UserIndicatorControllerMock()) userIndicatorController: UserIndicatorControllerMock())
} }
deinit {
AppSettings.resetAllSettings()
}
@Test
func initialState() {
#expect(context.viewState.topLevelSpaces.count == 3)
}
@Test
func topLevelSpacesSubscription() async throws {
var deferred = deferFulfillment(context.observe(\.viewState.topLevelSpaces)) { $0.count == 0 }
topLevelSpacesSubject.send([])
try await deferred.fulfill()
#expect(context.viewState.topLevelSpaces.count == 0)
deferred = deferFulfillment(context.observe(\.viewState.topLevelSpaces)) { $0.count == 1 }
topLevelSpacesSubject.send([
SpaceServiceRoom.mock(isSpace: true)
])
try await deferred.fulfill()
#expect(context.viewState.topLevelSpaces.count == 1)
}
@Test
func selectingSpace() async throws {
let selectedSpace = topLevelSpacesSubject.value[0]
let deferred = deferFulfillment(viewModel.actionsPublisher) { _ in true }
viewModel.context.send(viewAction: .spaceAction(.select(selectedSpace)))
let action = try await deferred.fulfill()
switch action {
case .selectSpace(let spaceRoomListProxy) where spaceRoomListProxy.id == selectedSpace.id:
break
default:
Issue.record("The action should select the space.")
}
}
@Test
func featureAnnouncement() async throws {
#expect(!appSettings.hasSeenSpacesAnnouncement)
#expect(!context.isPresentingFeatureAnnouncement)
let deferred = deferFulfillment(context.observe(\.isPresentingFeatureAnnouncement)) { $0 == true }
viewModel.context.send(viewAction: .screenAppeared)
try await deferred.fulfill()
#expect(context.isPresentingFeatureAnnouncement)
viewModel.context.send(viewAction: .featureAnnouncementAppeared)
#expect(appSettings.hasSeenSpacesAnnouncement)
context.isPresentingFeatureAnnouncement = false
let deferredFailure = deferFailure(context.observe(\.isPresentingFeatureAnnouncement), timeout: .seconds(1)) { $0 == true }
viewModel.context.send(viewAction: .screenAppeared)
try await deferredFailure.fulfill()
#expect(!context.isPresentingFeatureAnnouncement)
}
} }

View File

@@ -7,19 +7,20 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class StartChatScreenViewModelTests: XCTestCase { @Suite
var viewModel: StartChatScreenViewModelProtocol! struct StartChatScreenViewModelTests {
var clientProxy: ClientProxyMock! private var viewModel: StartChatScreenViewModelProtocol!
var userDiscoveryService: UserDiscoveryServiceMock! private var clientProxy: ClientProxyMock!
private var userDiscoveryService: UserDiscoveryServiceMock!
var context: StartChatScreenViewModel.Context { private var context: StartChatScreenViewModel.Context {
viewModel.context viewModel.context
} }
override func setUpWithError() throws { init() {
clientProxy = .init(.init(userID: "")) clientProxy = .init(.init(userID: ""))
userDiscoveryService = UserDiscoveryServiceMock() userDiscoveryService = UserDiscoveryServiceMock()
userDiscoveryService.searchProfilesWithReturnValue = .success([]) userDiscoveryService.searchProfilesWithReturnValue = .success([])
@@ -31,21 +32,23 @@ class StartChatScreenViewModelTests: XCTestCase {
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
} }
func testQueryShowingNoResults() async { @Test
mutating func queryShowingNoResults() async {
await search(query: "A") await search(query: "A")
XCTAssertEqual(context.viewState.usersSection.type, .suggestions) #expect(context.viewState.usersSection.type == .suggestions)
await search(query: "AA") await search(query: "AA")
XCTAssertEqual(context.viewState.usersSection.type, .suggestions) #expect(context.viewState.usersSection.type == .suggestions)
XCTAssertFalse(userDiscoveryService.searchProfilesWithCalled) #expect(!userDiscoveryService.searchProfilesWithCalled)
await search(query: "AAA") await search(query: "AAA")
assertSearchResults(toBe: 0) 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: [])) clientProxy.resolveRoomAliasReturnValue = .success(.init(roomId: "id", servers: []))
let deferredViewState = deferFulfillment(viewModel.context.$viewState) { viewState in let deferredViewState = deferFulfillment(viewModel.context.$viewState) { viewState in
@@ -61,7 +64,8 @@ class StartChatScreenViewModelTests: XCTestCase {
try await deferredAction.fulfill() try await deferredAction.fulfill()
} }
func testJoinRoomByAddressFailsBecauseInvalid() async throws { @Test
func joinRoomByAddressFailsBecauseInvalid() async throws {
let deferred = deferFulfillment(viewModel.context.$viewState) { viewState in let deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
viewState.joinByAddressState == .invalidAddress viewState.joinByAddressState == .invalidAddress
} }
@@ -70,7 +74,8 @@ class StartChatScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
} }
func testJoinRoomByAddressFailsBecauseNotFound() async throws { @Test
func joinRoomByAddressFailsBecauseNotFound() async throws {
clientProxy.resolveRoomAliasReturnValue = .failure(.failedResolvingRoomAlias) clientProxy.resolveRoomAliasReturnValue = .failure(.failedResolvingRoomAlias)
let deferred = deferFulfillment(viewModel.context.$viewState) { viewState in let deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
@@ -84,14 +89,14 @@ class StartChatScreenViewModelTests: XCTestCase {
// MARK: - Private // MARK: - Private
private func assertSearchResults(toBe count: Int) { private func assertSearchResults(toBe count: Int) {
XCTAssertTrue(count >= 0) #expect(count >= 0)
XCTAssertEqual(context.viewState.usersSection.type, .searchResult) #expect(context.viewState.usersSection.type == .searchResult)
XCTAssertEqual(context.viewState.usersSection.users.count, count) #expect(context.viewState.usersSection.users.count == count)
XCTAssertEqual(context.viewState.hasEmptySearchResults, count == 0) #expect(context.viewState.hasEmptySearchResults == (count == 0))
} }
@discardableResult @discardableResult
private func search(query: String) async -> StartChatScreenViewState? { private mutating func search(query: String) async -> StartChatScreenViewState? {
viewModel.context.searchQuery = query viewModel.context.searchQuery = query
return await context.$viewState.nextValue return await context.$viewState.nextValue
} }

View File

@@ -8,21 +8,19 @@
import Combine import Combine
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class StaticLocationScreenViewModelTests: XCTestCase { @Suite
let timelineProxy = TimelineProxyMock(.init()) struct StaticLocationScreenViewModelTests {
private let timelineProxy = TimelineProxyMock(.init())
private var viewModel: StaticLocationScreenViewModelProtocol
var viewModel: StaticLocationScreenViewModelProtocol! private var context: StaticLocationScreenViewModel.Context {
var context: StaticLocationScreenViewModel.Context {
viewModel.context viewModel.context
} }
private var cancellables = Set<AnyCancellable>() init() {
override func setUpWithError() throws {
cancellables.removeAll()
let viewModel = StaticLocationScreenViewModel(interactionMode: .picker, let viewModel = StaticLocationScreenViewModel(interactionMode: .picker,
mapURLBuilder: ServiceLocator.shared.settings.mapTilerConfiguration, mapURLBuilder: ServiceLocator.shared.settings.mapTilerConfiguration,
timelineController: MockTimelineController(timelineProxy: timelineProxy), timelineController: MockTimelineController(timelineProxy: timelineProxy),
@@ -32,81 +30,92 @@ class StaticLocationScreenViewModelTests: XCTestCase {
self.viewModel = viewModel self.viewModel = viewModel
} }
func testUserDidPan() { @Test
XCTAssertTrue(context.viewState.isSharingUserLocation) func userDidPan() {
XCTAssertEqual(context.showsUserLocationMode, .showAndFollow) #expect(context.viewState.isSharingUserLocation)
#expect(context.showsUserLocationMode == .showAndFollow)
context.send(viewAction: .userDidPan) context.send(viewAction: .userDidPan)
XCTAssertFalse(context.viewState.isSharingUserLocation) #expect(!context.viewState.isSharingUserLocation)
XCTAssertEqual(context.showsUserLocationMode, .show) #expect(context.showsUserLocationMode == .show)
} }
func testCenterOnUser() { @Test
XCTAssertTrue(context.viewState.isSharingUserLocation) func centerOnUser() {
#expect(context.viewState.isSharingUserLocation)
context.showsUserLocationMode = .show context.showsUserLocationMode = .show
XCTAssertFalse(context.viewState.isSharingUserLocation) #expect(!context.viewState.isSharingUserLocation)
context.send(viewAction: .centerToUser) context.send(viewAction: .centerToUser)
XCTAssertTrue(context.viewState.isSharingUserLocation) #expect(context.viewState.isSharingUserLocation)
XCTAssertEqual(context.showsUserLocationMode, .showAndFollow) #expect(context.showsUserLocationMode == .showAndFollow)
} }
func testCenterOnUserWithoutAuth() { @Test
func centerOnUserWithoutAuth() {
context.showsUserLocationMode = .hide context.showsUserLocationMode = .hide
context.isLocationAuthorized = nil context.isLocationAuthorized = nil
context.send(viewAction: .centerToUser) context.send(viewAction: .centerToUser)
XCTAssertEqual(context.showsUserLocationMode, .showAndFollow) #expect(context.showsUserLocationMode == .showAndFollow)
} }
func testCenterOnUserWithDeniedAuth() { @Test
func centerOnUserWithDeniedAuth() {
context.isLocationAuthorized = false context.isLocationAuthorized = false
context.showsUserLocationMode = .hide context.showsUserLocationMode = .hide
context.send(viewAction: .centerToUser) context.send(viewAction: .centerToUser)
XCTAssertNotEqual(context.showsUserLocationMode, .showAndFollow) #expect(context.showsUserLocationMode != .showAndFollow)
XCTAssertNotNil(context.alertInfo) #expect(context.alertInfo != nil)
} }
func testErrorMapping() { @Test
func errorMapping() {
let mapError = AlertInfo(locationSharingViewError: .mapError(.failedLoadingMap)) 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)) 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) 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.mapCenterLocation = .init(latitude: 0, longitude: 0)
context.geolocationUncertainty = 10 context.geolocationUncertainty = 10
let deferred = deferFulfillment(viewModel.actions) { $0 == .close } 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) try await confirmation { confirmation in
await fulfillment(of: [expectation], timeout: 1) timelineProxy.sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeClosure = { _, geoURI, _, _, assetType in
try await deferred.fulfill() #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.mapCenterLocation = .init(latitude: 0, longitude: 0)
context.isLocationAuthorized = nil context.isLocationAuthorized = nil
context.geolocationUncertainty = 10 context.geolocationUncertainty = 10
let deferred = deferFulfillment(viewModel.actions) { $0 == .close } 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) try await confirmation { confirmation in
await fulfillment(of: [expectation], timeout: 1) timelineProxy.sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeClosure = { _, geoURI, _, _, assetType in
try await deferred.fulfill() #expect(geoURI.uncertainty == nil)
#expect(assetType == .pin)
confirmation()
return .success(())
}
context.send(viewAction: .selectLocation)
try await deferred.fulfill()
}
} }
} }

View File

@@ -7,79 +7,90 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
class StringTests: XCTestCase { @Suite
func testEmptyIsAscii() { struct StringTests {
XCTAssertTrue("".isASCII) @Test
func emptyIsAscii() {
#expect("".isASCII)
} }
func testSpaceIsAscii() { @Test
XCTAssertTrue("".isASCII) func spaceIsAscii() {
#expect("".isASCII)
} }
func testJohnnyIsAscii() { @Test
XCTAssertTrue("johnny".isASCII) func johnnyIsAscii() {
#expect("johnny".isASCII)
} }
func testJöhnnyIsNotAscii() { @Test
XCTAssertFalse("jöhnny".isASCII) func jöhnnyIsNotAscii() {
#expect(!"jöhnny".isASCII)
} }
func testJ🅾hnnyIsNotAscii() { @Test
XCTAssertFalse("j🅾hnny".isASCII) func jEmojiHnnyIsNotAscii() {
#expect(!"j🅾hnny".isASCII)
} }
func testAsciifiedMethod() { @Test
func asciifiedMethod() {
// ASCII strings return themselves unchanged // ASCII strings return themselves unchanged
XCTAssertEqual("johnny".asciified(), "johnny") #expect("johnny".asciified() == "johnny")
XCTAssertEqual("hello".asciified(), "hello") #expect("hello".asciified() == "hello")
XCTAssertEqual("abc123".asciified(), "abc123") #expect("abc123".asciified() == "abc123")
XCTAssertEqual("".asciified(), "") #expect("".asciified() == "")
XCTAssertEqual(" ".asciified(), " ") #expect(" ".asciified() == " ")
// Non-ASCII strings get converted or stripped // Non-ASCII strings get converted or stripped
XCTAssertEqual("jöhnny".asciified(), "johnny", "ö should become o") #expect("jöhnny".asciified() == "johnny", "ö should become o")
XCTAssertEqual("jåhnny".asciified(), "jahnny", "å should become a") #expect("jåhnny".asciified() == "jahnny", "å should become a")
XCTAssertEqual("café".asciified(), "cafe") #expect("café".asciified() == "cafe")
XCTAssertEqual("naïve".asciified(), "naive") #expect("naïve".asciified() == "naive")
XCTAssertEqual("résumé".asciified(), "resume") #expect("résumé".asciified() == "resume")
XCTAssertEqual("🚀".asciified(), "") #expect("🚀".asciified() == "")
XCTAssertEqual("Heartbreak Hotel 🏩".asciified(), "Heartbreak Hotel", "The emoji should be stripped.") #expect("Heartbreak Hotel 🏩".asciified() == "Heartbreak Hotel", "The emoji should be stripped.")
XCTAssertEqual("1⃣2⃣3".asciified(), "123", "The emoji should be converted to ASCII.") #expect("1⃣2⃣3".asciified() == "123", "The emoji should be converted to ASCII.")
} }
func testGenerateBreakableWhitespaceEnd() { @Test
func generateBreakableWhitespaceEnd() {
var count = 5 var count = 5
var result = "\u{2066}" + String(repeating: "\u{2004}", count: count) + "\u{2800}" 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 count = 3
result = "\u{2066}" + String(repeating: "\u{2004}", count: count) + "\u{2800}" 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 count = 0
result = "" result = ""
XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, layoutDirection: .leftToRight), result) #expect(String.generateBreakableWhitespaceEnd(whitespaceCount: count, layoutDirection: .leftToRight) == result)
count = 4 count = 4
result = "\u{2067}" + String(repeating: "\u{2004}", count: count) + "\u{2800}" 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 count = 0
result = "" result = ""
XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, layoutDirection: .rightToLeft), result) #expect(String.generateBreakableWhitespaceEnd(whitespaceCount: count, layoutDirection: .rightToLeft) == result)
} }
func testEllipsizeWorks() { @Test
XCTAssertEqual("ellipsize".ellipsize(length: 5), "ellip…") func ellipsizeWorks() {
#expect("ellipsize".ellipsize(length: 5) == "ellip…")
} }
func testEllipsizeNotNeeded() { @Test
XCTAssertEqual("ellipsize".ellipsize(length: 15), "ellipsize") func ellipsizeNotNeeded() {
#expect("ellipsize".ellipsize(length: 15) == "ellipsize")
} }
func testReplaceBreakOccurrences() { @Test
func replaceBreakOccurrences() {
let input0 = "</p><p>" let input0 = "</p><p>"
let input1 = "</p>\n<p>" let input1 = "</p>\n<p>"
let input2 = "</p>\n\n<p>" let input2 = "</p>\n\n<p>"
@@ -94,11 +105,11 @@ class StringTests: XCTestCase {
let expectedOutput4 = "<p>a<br><br>b</p>" let expectedOutput4 = "<p>a<br><br>b</p>"
let expectedOutput5 = input5 let expectedOutput5 = input5
XCTAssertEqual(input0.replacingHtmlBreaksOccurrences(), expectedOutput0) #expect(input0.replacingHtmlBreaksOccurrences() == expectedOutput0)
XCTAssertEqual(input1.replacingHtmlBreaksOccurrences(), expectedOutput1) #expect(input1.replacingHtmlBreaksOccurrences() == expectedOutput1)
XCTAssertEqual(input2.replacingHtmlBreaksOccurrences(), expectedOutput2) #expect(input2.replacingHtmlBreaksOccurrences() == expectedOutput2)
XCTAssertEqual(input3.replacingHtmlBreaksOccurrences(), expectedOutput3) #expect(input3.replacingHtmlBreaksOccurrences() == expectedOutput3)
XCTAssertEqual(input4.replacingHtmlBreaksOccurrences(), expectedOutput4) #expect(input4.replacingHtmlBreaksOccurrences() == expectedOutput4)
XCTAssertEqual(input5.replacingHtmlBreaksOccurrences(), expectedOutput5) #expect(input5.replacingHtmlBreaksOccurrences() == expectedOutput5)
} }
} }

View File

@@ -7,10 +7,13 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Foundation
import Testing
final class TextBasedRoomTimelineTests: XCTestCase { @Suite
func testTextRoomTimelineItemWhitespaceEnd() { struct TextBasedRoomTimelineTests {
@Test
func textRoomTimelineItemWhitespaceEnd() {
let timestamp = Calendar.current.startOfDay(for: .now).addingTimeInterval(60 * 60) // 1:00 am let timestamp = Calendar.current.startOfDay(for: .now).addingTimeInterval(60 * 60) // 1:00 am
let timelineItem = TextRoomTimelineItem(id: .randomEvent, let timelineItem = TextRoomTimelineItem(id: .randomEvent,
timestamp: timestamp, timestamp: timestamp,
@@ -19,10 +22,11 @@ final class TextBasedRoomTimelineTests: XCTestCase {
canBeRepliedTo: true, canBeRepliedTo: true,
sender: .init(id: UUID().uuidString), sender: .init(id: UUID().uuidString),
content: .init(body: "Test")) 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 timestamp = Calendar.current.startOfDay(for: .now).addingTimeInterval(-60) // 11:59 pm
let timelineItem = TextRoomTimelineItem(id: .randomEvent, let timelineItem = TextRoomTimelineItem(id: .randomEvent,
timestamp: timestamp, timestamp: timestamp,
@@ -31,10 +35,11 @@ final class TextBasedRoomTimelineTests: XCTestCase {
canBeRepliedTo: true, canBeRepliedTo: true,
sender: .init(id: UUID().uuidString), sender: .init(id: UUID().uuidString),
content: .init(body: "Test")) 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 let timestamp = Date.mock
var timelineItem = TextRoomTimelineItem(id: .randomEvent, var timelineItem = TextRoomTimelineItem(id: .randomEvent,
timestamp: timestamp, timestamp: timestamp,
@@ -45,10 +50,11 @@ final class TextBasedRoomTimelineTests: XCTestCase {
content: .init(body: "Test")) content: .init(body: "Test"))
timelineItem.properties.isEdited = true timelineItem.properties.isEdited = true
let editedCount = L10n.commonEditedSuffix.count 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 let timestamp = Date.mock
var timelineItem = TextRoomTimelineItem(id: .randomEvent, var timelineItem = TextRoomTimelineItem(id: .randomEvent,
timestamp: timestamp, timestamp: timestamp,
@@ -60,6 +66,6 @@ final class TextBasedRoomTimelineTests: XCTestCase {
timelineItem.properties.isEdited = true timelineItem.properties.isEdited = true
timelineItem.properties.deliveryStatus = .sendingFailed(.unknown) timelineItem.properties.deliveryStatus = .sendingFailed(.unknown)
let editedCount = L10n.commonEditedSuffix.count let editedCount = L10n.commonEditedSuffix.count
XCTAssertEqual(timelineItem.additionalWhitespaces(), timestamp.formattedTime().count + editedCount + 5) #expect(timelineItem.additionalWhitespaces() == timestamp.formattedTime().count + editedCount + 5)
} }
} }

View File

@@ -8,11 +8,13 @@
@testable import ElementX @testable import ElementX
import MatrixRustSDK import MatrixRustSDK
import XCTest import Testing
@MainActor @MainActor
class TimelineItemFactoryTests: XCTestCase { @Suite
func testCallInvite() { struct TimelineItemFactoryTests {
@Test
func callInvite() throws {
let ownUserID = "@alice:matrix.org" let ownUserID = "@alice:matrix.org"
let senderUserID = "@bob:matrix.org" let senderUserID = "@bob:matrix.org"
@@ -24,19 +26,15 @@ class TimelineItemFactoryTests: XCTestCase {
let eventTimelineItemProxy = EventTimelineItemProxy(item: eventTimelineItem, uniqueID: .init("0")) let eventTimelineItemProxy = EventTimelineItemProxy(item: eventTimelineItem, uniqueID: .init("0"))
let item = factory.buildTimelineItem(for: eventTimelineItemProxy, isDM: false) let item = try #require(factory.buildTimelineItem(for: eventTimelineItemProxy, isDM: false) as? CallInviteRoomTimelineItem,
"Incorrect item type")
guard let item = item as? CallInviteRoomTimelineItem else { #expect(item.isReactable == false)
XCTFail("Incorrect item type") #expect(item.canBeRepliedTo == false)
return #expect(item.isEditable == false)
} #expect(item.sender == TimelineItemSender(id: senderUserID))
#expect(item.properties.isEdited == false)
XCTAssertEqual(item.isReactable, false) #expect(item.properties.reactions == [])
XCTAssertEqual(item.canBeRepliedTo, false) #expect(item.properties.deliveryStatus == nil)
XCTAssertEqual(item.isEditable, false)
XCTAssertEqual(item.sender, TimelineItemSender(id: senderUserID))
XCTAssertEqual(item.properties.isEdited, false)
XCTAssertEqual(item.properties.reactions, [])
XCTAssertEqual(item.properties.deliveryStatus, nil)
} }
} }

View File

@@ -7,87 +7,67 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Foundation
import Testing
class URLComponentsTests: XCTestCase { @Suite
func testAddFragmentQueryItems() { struct URLComponentsTests {
guard let url = URL(string: "https://test.matrix.org"), @Test
var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { func addFragmentQueryItems() throws {
XCTFail("URL invalid") let url = try #require(URL(string: "https://test.matrix.org"))
return 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")] let fragmentQueryItems: [URLQueryItem] = [.init(name: "first", value: "1"), .init(name: "second", value: "2")]
components.fragmentQueryItems = fragmentQueryItems 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() { @Test
guard let url = URL(string: "https://test.matrix.org#random/data?first=1&second=2"), func removeFragmentQueryItem() throws {
var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { let url = try #require(URL(string: "https://test.matrix.org#random/data?first=1&second=2"))
XCTFail("URL invalid") var components = try #require(URLComponents(url: url, resolvingAgainstBaseURL: true))
return var fragmentQueryItems = try #require(components.fragmentQueryItems)
}
XCTAssertNotNil(components.fragmentQueryItems)
guard var fragmentQueryItems = components.fragmentQueryItems else {
return
}
fragmentQueryItems.removeAll { $0.name == "first" } fragmentQueryItems.removeAll { $0.name == "first" }
components.fragmentQueryItems = fragmentQueryItems 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() { @Test
guard let url = URL(string: "https://test.matrix.org#/random/data?first=1&second=2"), func appendFragmentQueryItem() throws {
var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { let url = try #require(URL(string: "https://test.matrix.org#/random/data?first=1&second=2"))
XCTFail("URL invalid") var components = try #require(URLComponents(url: url, resolvingAgainstBaseURL: true))
return var fragmentQueryItems = try #require(components.fragmentQueryItems)
}
XCTAssertNotNil(components.fragmentQueryItems)
guard var fragmentQueryItems = components.fragmentQueryItems else {
return
}
fragmentQueryItems.insert(.init(name: "mr in between", value: "hello"), at: 1) fragmentQueryItems.insert(.init(name: "mr in between", value: "hello"), at: 1)
components.fragmentQueryItems = fragmentQueryItems 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() { @Test
guard let url = URL(string: "https://test.matrix.org#/random/data?first=1&second=2"), func changeFragmentQueryItemValue() throws {
var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { let url = try #require(URL(string: "https://test.matrix.org#/random/data?first=1&second=2"))
XCTFail("URL invalid") var components = try #require(URLComponents(url: url, resolvingAgainstBaseURL: true))
return var fragmentQueryItems = try #require(components.fragmentQueryItems)
}
XCTAssertNotNil(components.fragmentQueryItems)
guard var fragmentQueryItems = components.fragmentQueryItems else {
return
}
fragmentQueryItems[0].value = "last" fragmentQueryItems[0].value = "last"
components.fragmentQueryItems = fragmentQueryItems 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() { @Test
guard let url = URL(string: "https://call.element.io/room#/callName?appPrompt=true&confineToRoom=false"), func elementCallParameters() throws {
var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { let url = try #require(URL(string: "https://call.element.io/room#/callName?appPrompt=true&confineToRoom=false"))
XCTFail("URL invalid") var components = try #require(URLComponents(url: url, resolvingAgainstBaseURL: true))
return
}
components.fragmentQueryItems?.removeAll { $0.name == "appPrompt" } components.fragmentQueryItems?.removeAll { $0.name == "appPrompt" }
components.fragmentQueryItems?.removeAll { $0.name == "confineToRoom" } 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: "appPrompt", value: "false"))
components.fragmentQueryItems?.append(.init(name: "confineToRoom", value: "true")) 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")
} }
} }

View File

@@ -7,37 +7,37 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Foundation
import Testing
class URLTests: XCTestCase { @Suite
func testURLDirectoryName() { struct URLTests {
@Test
func urlDirectoryName() throws {
let url: URL = "https://matrix.example.com/foo/bar/" let url: URL = "https://matrix.example.com/foo/bar/"
let directoryName = url.asDirectoryName() let directoryName = url.asDirectoryName()
XCTAssertEqual(directoryName, "matrix.example.com-foo-bar") #expect(directoryName == "matrix.example.com-foo-bar")
createDirectory(with: directoryName) 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 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() 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") #expect(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) try createDirectory(with: directoryName)
} }
// MARK: - Helpers // MARK: - Helpers
func createDirectory(with directoryName: String) { func createDirectory(with directoryName: String) throws {
let url = URL.temporaryDirectory.appending(path: directoryName) let url = URL.temporaryDirectory.appending(path: directoryName)
try? FileManager.default.removeItem(at: url) try? FileManager.default.removeItem(at: url)
do { try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
} catch {
XCTFail("Invalid file path: \(error.localizedDescription)")
}
guard FileManager.default.directoryExists(at: url) else { guard FileManager.default.directoryExists(at: url) else {
XCTFail("Invalid file path") Issue.record("Invalid file path")
return return
} }
} }

View File

@@ -7,20 +7,24 @@
// //
@testable import ElementX @testable import ElementX
import XCTest import Testing
class UserAgentBuilderTests: XCTestCase { @Suite
func testIsNotNil() { struct UserAgentBuilderTests {
XCTAssertNotNil(UserAgentBuilder.makeASCIIUserAgent()) @Test
func isNotUnknow() {
#expect(UserAgentBuilder.makeASCIIUserAgent() != "unknown")
} }
func testContainsClientName() { @Test
func containsClientName() {
let userAgent = UserAgentBuilder.makeASCIIUserAgent() 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() 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")
} }
} }

View File

@@ -8,47 +8,53 @@
import Combine import Combine
@testable import ElementX @testable import ElementX
import XCTest import Testing
@MainActor @MainActor
class UserDetailsEditScreenViewModelTests: XCTestCase { @Suite
var viewModel: UserDetailsEditScreenViewModel! struct UserDetailsEditScreenViewModelTests {
private var viewModel: UserDetailsEditScreenViewModel!
private var userIndicatorController: UserIndicatorControllerMock!
var userIndicatorController: UserIndicatorControllerMock! private var context: UserDetailsEditScreenViewModelType.Context {
var context: UserDetailsEditScreenViewModelType.Context {
viewModel.context viewModel.context
} }
func testCannotSaveOnLanding() { init() {
setupViewModel() userIndicatorController = UserIndicatorControllerMock.default
XCTAssertFalse(context.viewState.canSave) viewModel = .init(userSession: UserSessionMock(.init()),
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings),
userIndicatorController: userIndicatorController)
} }
func testNameDidChange() { @Test
setupViewModel() func cannotSaveOnLanding() {
#expect(!context.viewState.canSave)
}
@Test
func nameDidChange() {
context.name = "name" context.name = "name"
XCTAssertTrue(context.viewState.nameDidChange) #expect(context.viewState.nameDidChange)
XCTAssertTrue(context.viewState.canSave) #expect(context.viewState.canSave)
} }
func testEmptyNameCannotBeSaved() { @Test
setupViewModel() func emptyNameCannotBeSaved() {
context.name = "" context.name = ""
XCTAssertFalse(context.viewState.canSave) #expect(!context.viewState.canSave)
} }
func testAvatarPickerShowsSheet() { @Test
setupViewModel() func avatarPickerShowsSheet() {
context.name = "name" context.name = "name"
XCTAssertFalse(context.showMediaSheet) #expect(!context.showMediaSheet)
context.send(viewAction: .presentMediaSource) context.send(viewAction: .presentMediaSource)
XCTAssertTrue(context.showMediaSheet) #expect(context.showMediaSheet)
} }
func testSave() async throws { @Test
setupViewModel() func save() async throws {
let deferred = deferFulfillment(viewModel.actions) { $0 == .dismiss } let deferred = deferFulfillment(viewModel.actions) { $0 == .dismiss }
context.name = "name" context.name = "name"
@@ -57,43 +63,33 @@ class UserDetailsEditScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
} }
func testCancelWithChangesAndDiscard() async throws { @Test
setupViewModel() func cancelWithChangesAndDiscard() async throws {
context.name = "name" context.name = "name"
XCTAssertTrue(context.viewState.canSave) #expect(context.viewState.canSave)
XCTAssertNil(context.alertInfo) #expect(context.alertInfo == nil)
context.send(viewAction: .cancel) context.send(viewAction: .cancel)
XCTAssertNotNil(context.alertInfo) #expect(context.alertInfo != nil)
let deferred = deferFulfillment(viewModel.actions) { $0 == .dismiss } let deferred = deferFulfillment(viewModel.actions) { $0 == .dismiss }
context.alertInfo?.secondaryButton?.action?() // Discard context.alertInfo?.secondaryButton?.action?() // Discard
try await deferred.fulfill() try await deferred.fulfill()
} }
func testCancelWithChangesAndSave() async throws { @Test
setupViewModel() func cancelWithChangesAndSave() async throws {
context.name = "name" context.name = "name"
XCTAssertTrue(context.viewState.canSave) #expect(context.viewState.canSave)
XCTAssertNil(context.alertInfo) #expect(context.alertInfo == nil)
context.send(viewAction: .cancel) context.send(viewAction: .cancel)
XCTAssertNotNil(context.alertInfo) #expect(context.alertInfo != nil)
let deferred = deferFulfillment(viewModel.actions) { $0 == .dismiss } let deferred = deferFulfillment(viewModel.actions) { $0 == .dismiss }
context.alertInfo?.primaryButton.action?() // Save context.alertInfo?.primaryButton.action?() // Save
try await deferred.fulfill() 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)
}
} }

Some files were not shown because too many files have changed in this diff Show More