Swift Testing for Unit Tests PART 1 (#5119)
* migrated a lot of unit tests to Swift Testing and added a new implementation for deferred fulfillment more tests migration Cleaned the code manually to establish some good patterns more code improvements some more code improvements removed empty tests update project * more pr suggestions and cleanups * removed the TestSetup pattern * fixing claude not reusing tests * pr suggestion + added indent rule to swiftformat so that we can prevent AIs to change that
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
--commas inline
|
--commas inline
|
||||||
--ifdef no-indent
|
--ifdef no-indent
|
||||||
|
--indent 4
|
||||||
--nospaceoperators ...,..<
|
--nospaceoperators ...,..<
|
||||||
--stripunusedargs closure-only
|
--stripunusedargs closure-only
|
||||||
--trimwhitespace nonblank-lines
|
--trimwhitespace nonblank-lines
|
||||||
|
|||||||
@@ -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 */,
|
||||||
|
|||||||
242
ElementX/Sources/Other/DeferredFulfillment.swift
Normal file
242
ElementX/Sources/Other/DeferredFulfillment.swift
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2025 Element Creations Ltd.
|
||||||
|
// Copyright 2023-2025 New Vector Ltd.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
// Please see LICENSE files in the repository root for full details.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
struct DeferredFulfillment<T> {
|
||||||
|
let closure: () async throws -> T
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func fulfill() async throws -> T {
|
||||||
|
try await closure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DeferredFulfillmentError: Error {
|
||||||
|
enum Kind {
|
||||||
|
case noOutput
|
||||||
|
case unexpectedFulfillment
|
||||||
|
}
|
||||||
|
|
||||||
|
let kind: Kind
|
||||||
|
let message: String?
|
||||||
|
|
||||||
|
static func noOutput(message: String?) -> Self {
|
||||||
|
.init(kind: .noOutput, message: message)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func unexpectedFulfillment(message: String?) -> Self {
|
||||||
|
.init(kind: .unexpectedFulfillment, message: message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Utility that assists in subscribing to a publisher and deferring the fulfilment and results until some other actions have been performed.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - publisher: The publisher to wait on.
|
||||||
|
/// - timeout: A timeout after which we give up.
|
||||||
|
/// - until: callback that evaluates outputs until some condition is reached
|
||||||
|
/// - Returns: The deferred fulfilment to be executed after some actions and that returns the result of the publisher.
|
||||||
|
func deferFulfillment<P: Publisher>(_ publisher: P,
|
||||||
|
timeout: Duration = .seconds(10),
|
||||||
|
message: String? = nil,
|
||||||
|
until condition: @escaping (P.Output) -> Bool) -> DeferredFulfillment<P.Output> {
|
||||||
|
var result: Result<P.Output, Error>?
|
||||||
|
var hasFulfilled = false
|
||||||
|
|
||||||
|
let cancellable = publisher
|
||||||
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
result = .failure(error)
|
||||||
|
hasFulfilled = true
|
||||||
|
case .finished:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} receiveValue: { value in
|
||||||
|
if condition(value), !hasFulfilled {
|
||||||
|
result = .success(value)
|
||||||
|
hasFulfilled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DeferredFulfillment<P.Output> {
|
||||||
|
let startTime = ContinuousClock.now
|
||||||
|
|
||||||
|
while !hasFulfilled {
|
||||||
|
await Task.yield()
|
||||||
|
if ContinuousClock.now - startTime >= timeout {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancellable.cancel()
|
||||||
|
|
||||||
|
guard let unwrappedResult = result else {
|
||||||
|
throw DeferredFulfillmentError.noOutput(message: message)
|
||||||
|
}
|
||||||
|
return try unwrappedResult.get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Utility that assists in observing an async sequence, deferring the fulfilment and results until some condition has been met.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - asyncSequence: The sequence to wait on.
|
||||||
|
/// - timeout: A timeout after which we give up.
|
||||||
|
/// - until: callback that evaluates outputs until some condition is reached
|
||||||
|
/// - Returns: The deferred fulfilment to be executed after some actions and that returns the result of the sequence.
|
||||||
|
func deferFulfillment<Value>(_ asyncSequence: any AsyncSequence<Value, Never>,
|
||||||
|
timeout: Duration = .seconds(10),
|
||||||
|
message: String? = nil,
|
||||||
|
until condition: @escaping (Value) -> Bool) -> DeferredFulfillment<Value> {
|
||||||
|
var result: Result<Value, Error>?
|
||||||
|
var hasFulfilled = false
|
||||||
|
|
||||||
|
let task = Task {
|
||||||
|
for await value in asyncSequence {
|
||||||
|
if condition(value), !hasFulfilled {
|
||||||
|
result = .success(value)
|
||||||
|
hasFulfilled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DeferredFulfillment<Value> {
|
||||||
|
let startTime = ContinuousClock.now
|
||||||
|
|
||||||
|
while !hasFulfilled {
|
||||||
|
await Task.yield()
|
||||||
|
if ContinuousClock.now - startTime >= timeout {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
guard let unwrappedResult = result else {
|
||||||
|
throw DeferredFulfillmentError.noOutput(message: message)
|
||||||
|
}
|
||||||
|
return try unwrappedResult.get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Utility that assists in subscribing to a publisher and deferring the fulfilment and results until some other actions have been performed.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - publisher: The publisher to wait on.
|
||||||
|
/// - keyPath: the key path for the expected values
|
||||||
|
/// - transitionValues: the values through which the keypath needs to transition through
|
||||||
|
/// - timeout: A timeout after which we give up.
|
||||||
|
/// - Returns: The deferred fulfilment to be executed after some actions and that returns the result of the publisher.
|
||||||
|
func deferFulfillment<P: Publisher, K: KeyPath<P.Output, V>, V: Equatable>(_ publisher: P,
|
||||||
|
keyPath: K,
|
||||||
|
transitionValues: [V],
|
||||||
|
timeout: Duration = .seconds(10)) -> DeferredFulfillment<P.Output> {
|
||||||
|
var expectedOrder = transitionValues
|
||||||
|
return deferFulfillment(publisher, timeout: timeout) { value in
|
||||||
|
let receivedValue = value[keyPath: keyPath]
|
||||||
|
if let index = expectedOrder.firstIndex(where: { $0 == receivedValue }), index == 0 {
|
||||||
|
expectedOrder.remove(at: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
return expectedOrder.isEmpty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Utility that assists in subscribing to an async sequence and deferring the fulfilment and results until some other actions have been performed.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - asyncSequence: The sequence to wait on.
|
||||||
|
/// - transitionValues: the values through which the sequence needs to transition through
|
||||||
|
/// - timeout: A timeout after which we give up.
|
||||||
|
/// - Returns: The deferred fulfilment to be executed after some actions and that returns the result of the sequence.
|
||||||
|
func deferFulfillment<Value: Equatable>(_ asyncSequence: any AsyncSequence<Value, Never>,
|
||||||
|
transitionValues: [Value],
|
||||||
|
timeout: Duration = .seconds(10)) -> DeferredFulfillment<Value> {
|
||||||
|
var expectedOrder = transitionValues
|
||||||
|
return deferFulfillment(asyncSequence, timeout: timeout) { value in
|
||||||
|
if let index = expectedOrder.firstIndex(where: { $0 == value }), index == 0 {
|
||||||
|
expectedOrder.remove(at: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
return expectedOrder.isEmpty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Utility that assists in subscribing to a publisher and deferring the failure for a particular value until some other actions have been performed.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - publisher: The publisher to wait on.
|
||||||
|
/// - timeout: A timeout after which we give up.
|
||||||
|
/// - until: callback that evaluates outputs until some condition is reached
|
||||||
|
/// - Returns: The deferred fulfilment to be executed after some actions. The publisher's result is not returned from this fulfilment.
|
||||||
|
func deferFailure<P: Publisher>(_ publisher: P,
|
||||||
|
timeout: Duration,
|
||||||
|
message: String? = nil,
|
||||||
|
until condition: @escaping (P.Output) -> Bool) -> DeferredFulfillment<Void> where P.Failure == Never {
|
||||||
|
var hasFulfilled = false
|
||||||
|
let cancellable = publisher
|
||||||
|
.sink { value in
|
||||||
|
if condition(value), !hasFulfilled {
|
||||||
|
hasFulfilled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DeferredFulfillment<Void> {
|
||||||
|
let startTime = ContinuousClock.now
|
||||||
|
|
||||||
|
while !hasFulfilled {
|
||||||
|
await Task.yield()
|
||||||
|
if ContinuousClock.now - startTime >= timeout {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancellable.cancel()
|
||||||
|
|
||||||
|
// For deferFailure, if hasFulfilled is true, it means the condition was met (which is a failure)
|
||||||
|
if hasFulfilled {
|
||||||
|
throw DeferredFulfillmentError.unexpectedFulfillment(message: message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Utility that assists in subscribing to an async sequence and deferring the failure for a particular value until some other actions have been performed.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - asyncSequence: The sequence to wait on.
|
||||||
|
/// - timeout: A timeout after which we give up.
|
||||||
|
/// - until: callback that evaluates outputs until some condition is reached
|
||||||
|
/// - Returns: The deferred fulfilment to be executed after some actions. The sequence's result is not returned from this fulfilment.
|
||||||
|
func deferFailure<Value>(_ asyncSequence: any AsyncSequence<Value, Never>,
|
||||||
|
timeout: Duration,
|
||||||
|
message: String? = nil,
|
||||||
|
until condition: @escaping (Value) -> Bool) -> DeferredFulfillment<Void> {
|
||||||
|
var hasFulfilled = false
|
||||||
|
|
||||||
|
let task = Task {
|
||||||
|
for await value in asyncSequence {
|
||||||
|
if condition(value), !hasFulfilled {
|
||||||
|
hasFulfilled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DeferredFulfillment<Void> {
|
||||||
|
let startTime = ContinuousClock.now
|
||||||
|
|
||||||
|
while !hasFulfilled {
|
||||||
|
await Task.yield()
|
||||||
|
if ContinuousClock.now - startTime >= timeout {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
// For deferFailure, if hasFulfilled is true, it means the condition was met (which is a failure)
|
||||||
|
if hasFulfilled {
|
||||||
|
throw DeferredFulfillmentError.unexpectedFulfillment(message: message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,33 +8,26 @@
|
|||||||
|
|
||||||
import AVKit
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -35,20 +28,27 @@ class AnalyticsSettingsScreenViewModelTests: XCTestCase {
|
|||||||
analytics: ServiceLocator.shared.analytics)
|
analytics: ServiceLocator.shared.analytics)
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,20 +270,21 @@ 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))
|
||||||
|
|
||||||
// No call to start
|
// No call to start
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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") == "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ class AttributedStringBuilderTests: XCTestCase {
|
|||||||
checkLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expectedLink: expectedLink.absoluteString, expectedRuns: 3)
|
checkLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expectedLink: expectedLink.absoluteString, expectedRuns: 3)
|
||||||
checkLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expectedLink: expectedLink.absoluteString, expectedRuns: 3)
|
checkLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expectedLink: expectedLink.absoluteString, expectedRuns: 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDefaultFont() {
|
func testDefaultFont() {
|
||||||
let htmlString = "<b>Test</b> <i>string</i> "
|
let htmlString = "<b>Test</b> <i>string</i> "
|
||||||
|
|
||||||
@@ -313,7 +313,7 @@ class AttributedStringBuilderTests: XCTestCase {
|
|||||||
|
|
||||||
XCTFail("Couldn't find blockquote")
|
XCTFail("Couldn't find blockquote")
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:enable line_length
|
// swiftlint:enable line_length
|
||||||
|
|
||||||
func testBlockquoteWithLink() {
|
func testBlockquoteWithLink() {
|
||||||
@@ -515,7 +515,7 @@ class AttributedStringBuilderTests: XCTestCase {
|
|||||||
checkAttachment(attributedString: attributedStringFromHTML, expectedRuns: 1)
|
checkAttachment(attributedString: attributedStringFromHTML, expectedRuns: 1)
|
||||||
let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
|
let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
|
||||||
checkAttachment(attributedString: attributedStringFromPlain, expectedRuns: 1)
|
checkAttachment(attributedString: attributedStringFromPlain, expectedRuns: 1)
|
||||||
|
|
||||||
let string2 = "Hello @room"
|
let string2 = "Hello @room"
|
||||||
let attributedStringFromHTML2 = attributedStringBuilder.fromHTML(string2)
|
let attributedStringFromHTML2 = attributedStringBuilder.fromHTML(string2)
|
||||||
checkAttachment(attributedString: attributedStringFromHTML2, expectedRuns: 2)
|
checkAttachment(attributedString: attributedStringFromHTML2, expectedRuns: 2)
|
||||||
@@ -824,7 +824,7 @@ class AttributedStringBuilderTests: XCTestCase {
|
|||||||
XCTFail("Could not build the attributed string")
|
XCTFail("Could not build the attributed string")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let link = attributedString.runs.first(where: { $0.link != nil })?.link else {
|
guard let link = attributedString.runs.first(where: { $0.link != nil })?.link else {
|
||||||
XCTFail("Couldn't find the link")
|
XCTFail("Couldn't find the link")
|
||||||
return
|
return
|
||||||
@@ -840,7 +840,7 @@ class AttributedStringBuilderTests: XCTestCase {
|
|||||||
XCTFail("Could not build the attributed string")
|
XCTFail("Could not build the attributed string")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let link = attributedString.runs.first(where: { $0.link != nil })?.link else {
|
guard let link = attributedString.runs.first(where: { $0.link != nil })?.link else {
|
||||||
XCTFail("Couldn't find the link")
|
XCTFail("Couldn't find the link")
|
||||||
return
|
return
|
||||||
@@ -1108,7 +1108,7 @@ class AttributedStringBuilderTests: XCTestCase {
|
|||||||
XCTAssertEqual(link.confirmationParameters?.internalURL.absoluteString, "https://matrix.org")
|
XCTAssertEqual(link.confirmationParameters?.internalURL.absoluteString, "https://matrix.org")
|
||||||
XCTAssertEqual(link.confirmationParameters?.displayString, "👉️ #room:matrix.org")
|
XCTAssertEqual(link.confirmationParameters?.displayString, "👉️ #room:matrix.org")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testMxExternalPaymentDetailsRemoved() {
|
func testMxExternalPaymentDetailsRemoved() {
|
||||||
var htmlString = "This is visible.<span data-msc4286-external-payment-details> But this is hidden <a href=\"https://matrix.org\">and this link too</a></span>"
|
var htmlString = "This is visible.<span data-msc4286-external-payment-details> But this is hidden <a href=\"https://matrix.org\">and this link too</a></span>"
|
||||||
|
|
||||||
@@ -1138,7 +1138,7 @@ class AttributedStringBuilderTests: XCTestCase {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func checkLinkIn(attributedString: AttributedString?, expectedLink: String, expectedRuns: Int) {
|
private func checkLinkIn(attributedString: AttributedString?, expectedLink: String, expectedRuns: Int) {
|
||||||
|
|||||||
@@ -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.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,53 +104,48 @@ 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
|
|
||||||
do {
|
|
||||||
await audioPlayerState.updateState(progress: -5.0)
|
|
||||||
XCTAssertEqual(audioPlayerState.progress, 0.0)
|
|
||||||
XCTAssertEqual(audioPlayerMock.seekToReceivedProgress, 0.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we try to set a progress > 1.0, the new progress must be 1.0
|
|
||||||
do {
|
|
||||||
await audioPlayerState.updateState(progress: 1.5)
|
|
||||||
XCTAssertEqual(audioPlayerState.progress, 1.0)
|
|
||||||
XCTAssertEqual(audioPlayerMock.seekToReceivedProgress, 1.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
// If we try to set a negative progress, the new progress must be 0.0
|
||||||
audioPlayerMock.state = .stopped
|
await audioPlayerState.updateState(progress: -5.0)
|
||||||
await audioPlayerState.updateState(progress: 0.4)
|
#expect(audioPlayerState.progress == 0.0)
|
||||||
XCTAssertEqual(audioPlayerState.progress, 0.4)
|
#expect(audioPlayerMock.seekToReceivedProgress == 0.0)
|
||||||
XCTAssertEqual(audioPlayerMock.seekToReceivedProgress, 0.4)
|
|
||||||
XCTAssertFalse(audioPlayerState.isPublishingProgress)
|
// If we try to set a progress > 1.0, the new progress must be 1.0
|
||||||
}
|
await audioPlayerState.updateState(progress: 1.5)
|
||||||
|
#expect(audioPlayerState.progress == 1.0)
|
||||||
do {
|
#expect(audioPlayerMock.seekToReceivedProgress == 1.0)
|
||||||
audioPlayerMock.state = .playing
|
|
||||||
await audioPlayerState.updateState(progress: 0.4)
|
audioPlayerMock.state = .stopped
|
||||||
XCTAssertEqual(audioPlayerState.progress, 0.4)
|
await audioPlayerState.updateState(progress: 0.4)
|
||||||
XCTAssertEqual(audioPlayerMock.seekToReceivedProgress, 0.4)
|
#expect(audioPlayerState.progress == 0.4)
|
||||||
XCTAssert(audioPlayerState.isPublishingProgress)
|
#expect(audioPlayerMock.seekToReceivedProgress == 0.4)
|
||||||
}
|
#expect(!audioPlayerState.isPublishingProgress)
|
||||||
|
|
||||||
|
audioPlayerMock.state = .playing
|
||||||
|
await audioPlayerState.updateState(progress: 0.4)
|
||||||
|
#expect(audioPlayerState.progress == 0.4)
|
||||||
|
#expect(audioPlayerMock.seekToReceivedProgress == 0.4)
|
||||||
|
#expect(audioPlayerState.isPublishingProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testHandlingAudioPlayerActionDidStartLoading() async throws {
|
@Test
|
||||||
|
func handlingAudioPlayerActionDidStartLoading() async throws {
|
||||||
audioPlayerState.attachAudioPlayer(audioPlayerMock)
|
audioPlayerState.attachAudioPlayer(audioPlayerMock)
|
||||||
|
|
||||||
let deferred = deferFulfillment(audioPlayerState.$playbackState) { action in
|
let deferred = deferFulfillment(audioPlayerState.$playbackState) { action in
|
||||||
switch action {
|
switch action {
|
||||||
case .loading:
|
case .loading:
|
||||||
@@ -157,15 +157,16 @@ 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)
|
||||||
audioPlayerState.attachAudioPlayer(audioPlayerMock)
|
audioPlayerState.attachAudioPlayer(audioPlayerMock)
|
||||||
|
|
||||||
let deferred = deferFulfillment(audioPlayerState.$playbackState) { action in
|
let deferred = deferFulfillment(audioPlayerState.$playbackState) { action in
|
||||||
switch action {
|
switch action {
|
||||||
case .readyToPlay:
|
case .readyToPlay:
|
||||||
@@ -179,15 +180,16 @@ 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)
|
||||||
|
|
||||||
let deferred = deferFulfillment(audioPlayerState.$playbackState) { action in
|
let deferred = deferFulfillment(audioPlayerState.$playbackState) { action in
|
||||||
switch action {
|
switch action {
|
||||||
case .playing:
|
case .playing:
|
||||||
@@ -199,16 +201,17 @@ 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)
|
||||||
|
|
||||||
let deferred = deferFulfillment(audioPlayerState.$playbackState) { action in
|
let deferred = deferFulfillment(audioPlayerState.$playbackState) { action in
|
||||||
switch action {
|
switch action {
|
||||||
case .stopped:
|
case .stopped:
|
||||||
@@ -220,16 +223,17 @@ 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)
|
||||||
|
|
||||||
let deferred = deferFulfillment(audioPlayerState.$playbackState) { action in
|
let deferred = deferFulfillment(audioPlayerState.$playbackState) { action in
|
||||||
switch action {
|
switch action {
|
||||||
case .stopped:
|
case .stopped:
|
||||||
@@ -241,16 +245,17 @@ 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)
|
||||||
|
|
||||||
let deferred = deferFulfillment(audioPlayerState.$playbackState) { action in
|
let deferred = deferFulfillment(audioPlayerState.$playbackState) { action in
|
||||||
switch action {
|
switch action {
|
||||||
case .stopped:
|
case .stopped:
|
||||||
@@ -262,16 +267,17 @@ 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
|
||||||
switch action {
|
switch action {
|
||||||
case .playing:
|
case .playing:
|
||||||
@@ -282,8 +288,8 @@ 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 {
|
||||||
case .error:
|
case .error:
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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!
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ class AudioRecorderStateTests: XCTestCase {
|
|||||||
private var audioRecorderActions: AnyPublisher<AudioRecorderAction, Never> {
|
private var audioRecorderActions: AnyPublisher<AudioRecorderAction, Never> {
|
||||||
audioRecorderActionsSubject.eraseToAnyPublisher()
|
audioRecorderActionsSubject.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func buildAudioRecorderMock() -> AudioRecorderMock {
|
private func buildAudioRecorderMock() -> AudioRecorderMock {
|
||||||
let audioRecorderMock = AudioRecorderMock()
|
let audioRecorderMock = AudioRecorderMock()
|
||||||
audioRecorderMock.isRecording = false
|
audioRecorderMock.isRecording = false
|
||||||
@@ -30,34 +31,38 @@ 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
|
||||||
switch action {
|
switch action {
|
||||||
case .recording:
|
case .recording:
|
||||||
@@ -69,12 +74,13 @@ 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
|
||||||
switch action {
|
switch action {
|
||||||
case .stopped:
|
case .stopped:
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,40 +49,43 @@ 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",
|
||||||
sdkGitSHA: "1234",
|
sdkGitSHA: "1234",
|
||||||
session: .mock,
|
session: .mock,
|
||||||
appHooks: AppHooks())
|
appHooks: AppHooks())
|
||||||
|
|
||||||
let bugReport = BugReport(userID: "@mock:client.com",
|
let bugReport = BugReport(userID: "@mock:client.com",
|
||||||
deviceID: nil,
|
deviceID: nil,
|
||||||
ed25519: nil,
|
ed25519: nil,
|
||||||
@@ -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,14 +114,14 @@ 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,
|
||||||
ed25519: nil,
|
ed25519: 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"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,15 +158,15 @@ private class MockURLProtocol: URLProtocol {
|
|||||||
client?.urlProtocolDidFinishLoading(self)
|
client?.urlProtocolDidFinishLoading(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func stopLoading() {
|
override func stopLoading() {
|
||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
|
|
||||||
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
|
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
|
||||||
request
|
request
|
||||||
}
|
}
|
||||||
|
|
||||||
override class func canInit(with request: URLRequest) -> Bool {
|
override class func canInit(with request: URLRequest) -> Bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
//
|
|
||||||
// Copyright 2025 Element Creations Ltd.
|
|
||||||
// Copyright 2022-2025 New Vector Ltd.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
||||||
// Please see LICENSE files in the repository root for full details.
|
|
||||||
//
|
|
||||||
|
|
||||||
@testable import ElementX
|
|
||||||
import XCTest
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
class CallScreenViewModelTests: XCTestCase { }
|
|
||||||
@@ -24,7 +24,7 @@ class ChatsTabFlowCoordinatorTests: XCTestCase {
|
|||||||
var detailCoordinator: CoordinatorProtocol? {
|
var detailCoordinator: CoordinatorProtocol? {
|
||||||
splitCoordinator?.detailCoordinator
|
splitCoordinator?.detailCoordinator
|
||||||
}
|
}
|
||||||
|
|
||||||
var detailNavigationStack: NavigationStackCoordinator? {
|
var detailNavigationStack: NavigationStackCoordinator? {
|
||||||
detailCoordinator as? NavigationStackCoordinator
|
detailCoordinator as? NavigationStackCoordinator
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ final class CompletionSuggestionServiceTests: XCTestCase {
|
|||||||
let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
|
let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
|
||||||
let service = CompletionSuggestionService(roomProxy: roomProxyMock,
|
let service = CompletionSuggestionService(roomProxy: roomProxyMock,
|
||||||
roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
|
roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
|
||||||
|
|
||||||
var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||||
suggestions == []
|
suggestions == []
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,7 @@ final class CompletionSuggestionServiceTests: XCTestCase {
|
|||||||
let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
|
let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
|
||||||
let service = CompletionSuggestionService(roomProxy: roomProxyMock,
|
let service = CompletionSuggestionService(roomProxy: roomProxyMock,
|
||||||
roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
|
roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
|
||||||
|
|
||||||
var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||||
suggestions == []
|
suggestions == []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
|||||||
private var viewModel: ComposerToolbarViewModel!
|
private var viewModel: ComposerToolbarViewModel!
|
||||||
private var completionSuggestionServiceMock: CompletionSuggestionServiceMock!
|
private var completionSuggestionServiceMock: CompletionSuggestionServiceMock!
|
||||||
private var draftServiceMock: ComposerDraftServiceMock!
|
private var draftServiceMock: ComposerDraftServiceMock!
|
||||||
|
|
||||||
override func setUp() {
|
override func setUp() {
|
||||||
AppSettings.resetAllSettings()
|
AppSettings.resetAllSettings()
|
||||||
appSettings = AppSettings()
|
appSettings = AppSettings()
|
||||||
@@ -30,14 +30,14 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
|||||||
override func tearDown() {
|
override func tearDown() {
|
||||||
AppSettings.resetAllSettings()
|
AppSettings.resetAllSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
func testComposerFocus() {
|
func testComposerFocus() {
|
||||||
viewModel.process(timelineAction: .setMode(mode: .edit(originalEventOrTransactionID: .eventID("mock"), type: .default)))
|
viewModel.process(timelineAction: .setMode(mode: .edit(originalEventOrTransactionID: .eventID("mock"), type: .default)))
|
||||||
XCTAssertTrue(viewModel.state.bindings.composerFocused)
|
XCTAssertTrue(viewModel.state.bindings.composerFocused)
|
||||||
viewModel.process(timelineAction: .removeFocus)
|
viewModel.process(timelineAction: .removeFocus)
|
||||||
XCTAssertFalse(viewModel.state.bindings.composerFocused)
|
XCTAssertFalse(viewModel.state.bindings.composerFocused)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testComposerMode() {
|
func testComposerMode() {
|
||||||
let mode: ComposerMode = .edit(originalEventOrTransactionID: .eventID("mock"), type: .default)
|
let mode: ComposerMode = .edit(originalEventOrTransactionID: .eventID("mock"), type: .default)
|
||||||
viewModel.process(timelineAction: .setMode(mode: mode))
|
viewModel.process(timelineAction: .setMode(mode: mode))
|
||||||
@@ -45,7 +45,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
|||||||
viewModel.process(timelineAction: .clear)
|
viewModel.process(timelineAction: .clear)
|
||||||
XCTAssertEqual(viewModel.state.composerMode, .default)
|
XCTAssertEqual(viewModel.state.composerMode, .default)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testComposerModeIsPublished() {
|
func testComposerModeIsPublished() {
|
||||||
let mode: ComposerMode = .edit(originalEventOrTransactionID: .eventID("mock"), type: .default)
|
let mode: ComposerMode = .edit(originalEventOrTransactionID: .eventID("mock"), type: .default)
|
||||||
let expectation = expectation(description: "Composer mode is published")
|
let expectation = expectation(description: "Composer mode is published")
|
||||||
@@ -59,22 +59,22 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
|||||||
XCTAssertEqual(composerMode, mode)
|
XCTAssertEqual(composerMode, mode)
|
||||||
expectation.fulfill()
|
expectation.fulfill()
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.process(timelineAction: .setMode(mode: mode))
|
viewModel.process(timelineAction: .setMode(mode: mode))
|
||||||
|
|
||||||
wait(for: [expectation], timeout: 2.0)
|
wait(for: [expectation], timeout: 2.0)
|
||||||
cancellable.cancel()
|
cancellable.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
func testHandleKeyCommand() {
|
func testHandleKeyCommand() {
|
||||||
XCTAssertTrue(viewModel.context.viewState.keyCommands.count == 1)
|
XCTAssertTrue(viewModel.context.viewState.keyCommands.count == 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testComposerFocusAfterEnablingRTE() {
|
func testComposerFocusAfterEnablingRTE() {
|
||||||
viewModel.process(viewAction: .enableTextFormatting)
|
viewModel.process(viewAction: .enableTextFormatting)
|
||||||
XCTAssertTrue(viewModel.state.bindings.composerFocused)
|
XCTAssertTrue(viewModel.state.bindings.composerFocused)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRTEEnabledAfterSendingMessage() {
|
func testRTEEnabledAfterSendingMessage() {
|
||||||
viewModel.process(viewAction: .enableTextFormatting)
|
viewModel.process(viewAction: .enableTextFormatting)
|
||||||
XCTAssertTrue(viewModel.state.bindings.composerFocused)
|
XCTAssertTrue(viewModel.state.bindings.composerFocused)
|
||||||
@@ -82,7 +82,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
|||||||
viewModel.process(viewAction: .sendMessage)
|
viewModel.process(viewAction: .sendMessage)
|
||||||
XCTAssertTrue(viewModel.state.bindings.composerFormattingEnabled)
|
XCTAssertTrue(viewModel.state.bindings.composerFormattingEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAlertIsShownAfterLinkAction() {
|
func testAlertIsShownAfterLinkAction() {
|
||||||
XCTAssertNil(viewModel.state.bindings.alertInfo)
|
XCTAssertNil(viewModel.state.bindings.alertInfo)
|
||||||
viewModel.process(viewAction: .enableTextFormatting)
|
viewModel.process(viewAction: .enableTextFormatting)
|
||||||
@@ -139,7 +139,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
|||||||
viewModel.context.send(viewAction: .selectedSuggestion(suggestion))
|
viewModel.context.send(viewAction: .selectedSuggestion(suggestion))
|
||||||
|
|
||||||
// The display name can be used for HTML injection in the rich text editor and it's useless anyway as the clients don't use it when resolving display names
|
// The display name can be used for HTML injection in the rich text editor and it's useless anyway as the clients don't use it when resolving display names
|
||||||
|
|
||||||
XCTAssertEqual(wysiwygViewModel.content.html, "<a href=\"https://matrix.to/#/%23room-alias:matrix.org\">#room-alias:matrix.org</a> ")
|
XCTAssertEqual(wysiwygViewModel.content.html, "<a href=\"https://matrix.to/#/%23room-alias:matrix.org\">#room-alias:matrix.org</a> ")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,7 +345,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
|||||||
waveform: .data(waveformData),
|
waveform: .data(waveformData),
|
||||||
isUploading: false)))
|
isUploading: false)))
|
||||||
viewModel.saveDraft()
|
viewModel.saveDraft()
|
||||||
|
|
||||||
await fulfillment(of: [expectation], timeout: 10)
|
await fulfillment(of: [expectation], timeout: 10)
|
||||||
XCTAssertFalse(draftServiceMock.saveDraftCalled)
|
XCTAssertFalse(draftServiceMock.saveDraftCalled)
|
||||||
XCTAssertEqual(draftServiceMock.clearDraftCallsCount, 1)
|
XCTAssertEqual(draftServiceMock.clearDraftCallsCount, 1)
|
||||||
@@ -588,7 +588,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
|||||||
viewModel.context.composerFormattingEnabled = false
|
viewModel.context.composerFormattingEnabled = false
|
||||||
let text = "Hello @room"
|
let text = "Hello @room"
|
||||||
viewModel.process(timelineAction: .setText(plainText: text, htmlText: nil))
|
viewModel.process(timelineAction: .setText(plainText: text, htmlText: nil))
|
||||||
|
|
||||||
let deferred = deferFulfillment(viewModel.actions) { action in
|
let deferred = deferFulfillment(viewModel.actions) { action in
|
||||||
switch action {
|
switch action {
|
||||||
case let .sendMessage(plainText, _, _, intentionalMentions):
|
case let .sendMessage(plainText, _, _, intentionalMentions):
|
||||||
@@ -673,7 +673,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
|||||||
roomProxyMock.getMemberUserIDClosure = { _ in
|
roomProxyMock.getMemberUserIDClosure = { _ in
|
||||||
.success(roomMemberProxyMock)
|
.success(roomMemberProxyMock)
|
||||||
}
|
}
|
||||||
|
|
||||||
let mockSubject = CurrentValueSubject<[IdentityStatusChange], Never>([])
|
let mockSubject = CurrentValueSubject<[IdentityStatusChange], Never>([])
|
||||||
roomProxyMock.underlyingIdentityStatusChangesPublisher = mockSubject.asCurrentValuePublisher()
|
roomProxyMock.underlyingIdentityStatusChangesPublisher = mockSubject.asCurrentValuePublisher()
|
||||||
|
|
||||||
@@ -712,7 +712,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
|||||||
return .failure(.sdkError(ClientProxyMockError.generic))
|
return .failure(.sdkError(ClientProxyMockError.generic))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// There are 2 violations, ensure that resolving the first one is not enough
|
// There are 2 violations, ensure that resolving the first one is not enough
|
||||||
let mockSubject = CurrentValueSubject<[IdentityStatusChange], Never>([
|
let mockSubject = CurrentValueSubject<[IdentityStatusChange], Never>([
|
||||||
IdentityStatusChange(userId: "@alice:localhost", changedTo: .verificationViolation),
|
IdentityStatusChange(userId: "@alice:localhost", changedTo: .verificationViolation),
|
||||||
|
|||||||
@@ -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)!)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func minimalDateFormatting() throws {
|
||||||
|
let today = try #require(calendar.date(byAdding: DateComponents(hour: 9, minute: 30), to: startOfToday))
|
||||||
|
#expect(today.formattedMinimal() == today.formatted(date: .omitted, time: .shortened))
|
||||||
|
|
||||||
let yesterday = try XCTUnwrap(calendar.date(byAdding: .hour, value: 1, to: startOfYesterday))
|
let yesterday = try #require(calendar.date(byAdding: .hour, value: 1, to: startOfYesterday))
|
||||||
XCTAssertEqual(yesterday.formattedDateSeparator(), "Yesterday")
|
#expect(yesterday.formattedMinimal() == yesterday.formatted(Date.RelativeFormatStyle(presentation: .named, capitalizationContext: .beginningOfSentence)))
|
||||||
|
|
||||||
let nearYesterday = try XCTUnwrap(calendar.date(byAdding: DateComponents(hour: -10), to: today))
|
let nearYesterday = try #require(calendar.date(byAdding: DateComponents(hour: -10), to: today))
|
||||||
XCTAssertEqual(nearYesterday.formattedDateSeparator(), yesterday.formatted(Date.RelativeFormatStyle(presentation: .named, capitalizationContext: .beginningOfSentence)))
|
#expect(nearYesterday.formattedMinimal() == yesterday.formatted(Date.RelativeFormatStyle(presentation: .named, capitalizationContext: .beginningOfSentence)))
|
||||||
|
|
||||||
let threeDaysAgo = try XCTUnwrap(calendar.date(byAdding: .day, value: -3, to: startOfToday))
|
let threeDaysAgo = try #require(calendar.date(byAdding: .day, value: -3, to: startOfToday))
|
||||||
XCTAssertEqual(threeDaysAgo.formattedDateSeparator(), threeDaysAgo.formatted(.dateTime.weekday(.wide)))
|
#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
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
//
|
|
||||||
// Copyright 2025 Element Creations Ltd.
|
|
||||||
// Copyright 2022-2025 New Vector Ltd.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
||||||
// Please see LICENSE files in the repository root for full details.
|
|
||||||
//
|
|
||||||
|
|
||||||
@testable import ElementX
|
|
||||||
import XCTest
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
class DeveloperOptionsScreenViewModelTests: XCTestCase { }
|
|
||||||
@@ -76,7 +76,7 @@ class EditRoomAddressScreenViewModelTests: XCTestCase {
|
|||||||
|
|
||||||
XCTAssertNil(roomProxy.infoPublisher.value.canonicalAlias)
|
XCTAssertNil(roomProxy.infoPublisher.value.canonicalAlias)
|
||||||
XCTAssertEqual(viewModel.context.viewState.bindings.desiredAliasLocalPart, "room-name")
|
XCTAssertEqual(viewModel.context.viewState.bindings.desiredAliasLocalPart, "room-name")
|
||||||
|
|
||||||
let publishingExpectation = expectation(description: "Wait for publishing")
|
let publishingExpectation = expectation(description: "Wait for publishing")
|
||||||
roomProxy.publishRoomAliasInRoomDirectoryClosure = { roomAlias in
|
roomProxy.publishRoomAliasInRoomDirectoryClosure = { roomAlias in
|
||||||
defer { publishingExpectation.fulfill() }
|
defer { publishingExpectation.fulfill() }
|
||||||
@@ -107,7 +107,7 @@ class EditRoomAddressScreenViewModelTests: XCTestCase {
|
|||||||
userIndicatorController: UserIndicatorControllerMock())
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
|
|
||||||
context.desiredAliasLocalPart = "room-name"
|
context.desiredAliasLocalPart = "room-name"
|
||||||
|
|
||||||
let publishingExpectation = expectation(description: "Wait for publishing")
|
let publishingExpectation = expectation(description: "Wait for publishing")
|
||||||
roomProxy.publishRoomAliasInRoomDirectoryClosure = { roomAlias in
|
roomProxy.publishRoomAliasInRoomDirectoryClosure = { roomAlias in
|
||||||
defer { publishingExpectation.fulfill() }
|
defer { publishingExpectation.fulfill() }
|
||||||
@@ -144,7 +144,7 @@ class EditRoomAddressScreenViewModelTests: XCTestCase {
|
|||||||
userIndicatorController: UserIndicatorControllerMock())
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
|
|
||||||
context.desiredAliasLocalPart = "room-name"
|
context.desiredAliasLocalPart = "room-name"
|
||||||
|
|
||||||
let publishingExpectation = expectation(description: "Wait for publishing")
|
let publishingExpectation = expectation(description: "Wait for publishing")
|
||||||
roomProxy.publishRoomAliasInRoomDirectoryClosure = { roomAlias in
|
roomProxy.publishRoomAliasInRoomDirectoryClosure = { roomAlias in
|
||||||
defer { publishingExpectation.fulfill() }
|
defer { publishingExpectation.fulfill() }
|
||||||
|
|||||||
@@ -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) {
|
||||||
let pkPushPayloadMock = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 30)
|
confirmation()
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,20 +37,21 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -108,7 +111,7 @@ class HomeScreenViewModelTests: XCTestCase {
|
|||||||
room.leaveRoomClosure = { .failure(.sdkError(ClientProxyMockError.generic)) }
|
room.leaveRoomClosure = { .failure(.sdkError(ClientProxyMockError.generic)) }
|
||||||
|
|
||||||
clientProxy.roomForIdentifierClosure = { _ in .joined(room) }
|
clientProxy.roomForIdentifierClosure = { _ in .joined(room) }
|
||||||
|
|
||||||
let deferred = deferFulfillment(context.$viewState) { value in
|
let deferred = deferFulfillment(context.$viewState) { value in
|
||||||
value.bindings.alertInfo != nil
|
value.bindings.alertInfo != nil
|
||||||
}
|
}
|
||||||
@@ -116,39 +119,35 @@ class HomeScreenViewModelTests: XCTestCase {
|
|||||||
context.send(viewAction: .confirmLeaveRoom(roomIdentifier: mockRoomID))
|
context.send(viewAction: .confirmLeaveRoom(roomIdentifier: mockRoomID))
|
||||||
|
|
||||||
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 {
|
||||||
|
|||||||
@@ -8,55 +8,60 @@
|
|||||||
|
|
||||||
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!
|
||||||
|
|
||||||
var context: InviteUsersScreenViewModel.Context {
|
var context: InviteUsersScreenViewModel.Context {
|
||||||
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()),
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class JoinRoomScreenViewModelTests: XCTestCase {
|
|||||||
clientProxy = nil
|
clientProxy = nil
|
||||||
AppSettings.resetAllSettings()
|
AppSettings.resetAllSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
func testInteraction() async throws {
|
func testInteraction() async throws {
|
||||||
XCTAssertTrue(appSettings.seenInvites.isEmpty, "There shouldn't be any seen invites before running the tests.")
|
XCTAssertTrue(appSettings.seenInvites.isEmpty, "There shouldn't be any seen invites before running the tests.")
|
||||||
|
|
||||||
|
|||||||
@@ -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.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 &&
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
//
|
|
||||||
// Copyright 2025 Element Creations Ltd.
|
|
||||||
// Copyright 2022-2025 New Vector Ltd.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
||||||
// Please see LICENSE files in the repository root for full details.
|
|
||||||
//
|
|
||||||
|
|
||||||
@testable import ElementX
|
|
||||||
import XCTest
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
class LegalInformationScreenViewModelTests: XCTestCase { }
|
|
||||||
@@ -7,72 +7,79 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
@testable import ElementX
|
@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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,57 +8,62 @@
|
|||||||
|
|
||||||
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,
|
||||||
size: .init(width: 300, height: 200),
|
size: .init(width: 300, height: 200),
|
||||||
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,
|
||||||
size: .init(width: 300, height: 200),
|
size: .init(width: 300, height: 200),
|
||||||
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() {
|
|
||||||
let url = builder.interactiveMapURL(for: .dark)
|
|
||||||
let expectedURL: URL = "http://www.foo.com/dea61faf-292b-4774-9660-58fcef89a7f3/style.json?key=some_key"
|
|
||||||
XCTAssertEqual(url, expectedURL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testNilAPIKey() {
|
@Test
|
||||||
|
func dynamicMapBuilder() {
|
||||||
|
let url = builder.interactiveMapURL(for: .dark)
|
||||||
|
let expectedURL: URL = "http://www.foo.com/dea61faf-292b-4774-9660-58fcef89a7f3/style.json?key=some_key"
|
||||||
|
#expect(url == expectedURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
mutating func nilAPIKey() {
|
||||||
let configuration = MapTilerConfiguration(baseURL: Self.baseURL,
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
|
||||||
XCTAssertTrue(MatrixEntityRegex.isMatrixUserIdentifier("@username:example.com"))
|
|
||||||
XCTAssertFalse(MatrixEntityRegex.isMatrixUserIdentifier("username:example.com"))
|
|
||||||
XCTAssertFalse(MatrixEntityRegex.isMatrixUserIdentifier("@username.example.com"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRoomAlias() {
|
@Test
|
||||||
XCTAssertTrue(MatrixEntityRegex.isMatrixRoomAlias("#element-ios:matrix.org"))
|
func userID() {
|
||||||
XCTAssertFalse(MatrixEntityRegex.isMatrixRoomAlias("element-ios:matrix.org"))
|
#expect(MatrixEntityRegex.isMatrixUserIdentifier("@username:example.com"))
|
||||||
XCTAssertFalse(MatrixEntityRegex.isMatrixRoomAlias("#element-ios.matrix.org"))
|
#expect(!MatrixEntityRegex.isMatrixUserIdentifier("username:example.com"))
|
||||||
|
#expect(!MatrixEntityRegex.isMatrixUserIdentifier("@username.example.com"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testMatrixURI() {
|
@Test
|
||||||
|
func roomAlias() {
|
||||||
|
#expect(MatrixEntityRegex.isMatrixRoomAlias("#element-ios:matrix.org"))
|
||||||
|
#expect(!MatrixEntityRegex.isMatrixRoomAlias("element-ios:matrix.org"))
|
||||||
|
#expect(!MatrixEntityRegex.isMatrixRoomAlias("#element-ios.matrix.org"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func matrixURI() {
|
||||||
// Users
|
// 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"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,19 +227,19 @@ class MediaUploadPreviewScreenViewModelTests: XCTestCase {
|
|||||||
private var audioURL: URL {
|
private var audioURL: URL {
|
||||||
assertResourceURL(filename: "test_audio.mp3")
|
assertResourceURL(filename: "test_audio.mp3")
|
||||||
}
|
}
|
||||||
|
|
||||||
private var fileURL: URL {
|
private var fileURL: URL {
|
||||||
assertResourceURL(filename: "test_pdf.pdf")
|
assertResourceURL(filename: "test_pdf.pdf")
|
||||||
}
|
}
|
||||||
|
|
||||||
private var imageURL: URL {
|
private var imageURL: URL {
|
||||||
assertResourceURL(filename: "test_animated_image.gif")
|
assertResourceURL(filename: "test_animated_image.gif")
|
||||||
}
|
}
|
||||||
|
|
||||||
private var videoURL: URL {
|
private var videoURL: URL {
|
||||||
assertResourceURL(filename: "landscape_test_video.mov")
|
assertResourceURL(filename: "landscape_test_video.mov")
|
||||||
}
|
}
|
||||||
|
|
||||||
private var badImageURL = URL(filePath: "/home/user/this_file_doesn't_exist.jpg")
|
private var badImageURL = URL(filePath: "/home/user/this_file_doesn't_exist.jpg")
|
||||||
|
|
||||||
private func assertResourceURL(filename: String) -> URL {
|
private func assertResourceURL(filename: String) -> URL {
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ final class MediaUploadingPreprocessorTests: XCTestCase {
|
|||||||
XCTAssertEqual(optimizedVideoInfo.height, 720)
|
XCTAssertEqual(optimizedVideoInfo.height, 720)
|
||||||
XCTAssertEqual(optimizedVideoInfo.duration ?? 0, 30, accuracy: 100)
|
XCTAssertEqual(optimizedVideoInfo.duration ?? 0, 30, accuracy: 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testPortraitMp4VideoProcessing() async {
|
func testPortraitMp4VideoProcessing() async {
|
||||||
// Allow an increased execution time as we encode the video twice now.
|
// Allow an increased execution time as we encode the video twice now.
|
||||||
executionTimeAllowance = 180
|
executionTimeAllowance = 180
|
||||||
|
|||||||
@@ -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 {
|
init() {
|
||||||
cancellables.removeAll()
|
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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() },
|
||||||
navigationTabCoordinator.setTabs([
|
.init(coordinator: spacesCoordinator, details: .init(tag: .spaces, title: "Spaces", icon: \.space, selectedIcon: \.spaceSolid)) { confirm() }
|
||||||
.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])
|
||||||
])
|
|
||||||
assertCoordinatorsEqual(navigationTabCoordinator.tabCoordinators, [chatsCoordinator, spacesCoordinator])
|
navigationTabCoordinator.setTabs([.init(coordinator: SomeTestCoordinator(), details: .init(tag: .tab, title: "Tab", icon: \.help, selectedIcon: \.helpSolid))])
|
||||||
|
|
||||||
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.setSheetCoordinator(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
navigationTabCoordinator.setFullScreenCoverCoordinator(nil)
|
|
||||||
waitForExpectations(timeout: 1.0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testOverlayDismissalCallback() {
|
@Test
|
||||||
|
mutating func fullScreenCoverDismissalCallback() async {
|
||||||
|
let coordinator = SomeTestCoordinator()
|
||||||
|
await confirmation("Wait for callback") { confirm in
|
||||||
|
navigationTabCoordinator.setFullScreenCoverCoordinator(coordinator) {
|
||||||
|
confirm()
|
||||||
|
}
|
||||||
|
|
||||||
|
navigationTabCoordinator.setFullScreenCoverCoordinator(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
mutating func overlayDismissalCallback() async {
|
||||||
let overlayCoordinator = SomeTestCoordinator()
|
let 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)
|
||||||
|
try await Task.sleep(for: .seconds(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
navigationTabCoordinator.setOverlayPresentationMode(.minimized)
|
|
||||||
waitForExpectations(timeout: 1.0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 == [])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -7,149 +7,148 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
@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!
|
||||||
|
|
||||||
var context: RoomChangeRolesScreenViewModelType.Context {
|
var context: RoomChangeRolesScreenViewModelType.Context {
|
||||||
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() {
|
|
||||||
setupViewModel(mode: .moderator)
|
|
||||||
XCTAssertEqual(context.viewState.membersToPromote, [])
|
|
||||||
XCTAssertEqual(context.viewState.membersToDemote, [])
|
|
||||||
XCTAssertEqual(context.viewState.administrators, context.viewState.visibleAdministrators)
|
|
||||||
XCTAssertEqual(context.viewState.moderators, context.viewState.visibleModerators)
|
|
||||||
XCTAssertEqual(context.viewState.users, context.viewState.visibleUsers)
|
|
||||||
XCTAssertEqual(context.viewState.membersWithRole.count, 3)
|
|
||||||
XCTAssertNotNil(context.viewState.membersWithRole.first { $0.id == RoomMemberProxyMock.mockModerator.userID })
|
|
||||||
XCTAssertFalse(context.viewState.hasChanges)
|
|
||||||
XCTAssertFalse(context.viewState.isSearching)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testToggleUserOn() {
|
@Test
|
||||||
testInitialStateModerators()
|
mutating func initialStateModerators() {
|
||||||
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.")
|
#expect(context.viewState.membersToPromote == [])
|
||||||
return
|
#expect(context.viewState.membersToDemote == [])
|
||||||
}
|
#expect(context.viewState.administrators == context.viewState.visibleAdministrators)
|
||||||
|
#expect(context.viewState.moderators == context.viewState.visibleModerators)
|
||||||
|
#expect(context.viewState.users == context.viewState.visibleUsers)
|
||||||
|
#expect(context.viewState.membersWithRole.count == 3)
|
||||||
|
#expect(context.viewState.membersWithRole.first { $0.id == RoomMemberProxyMock.mockModerator.userID } != nil)
|
||||||
|
#expect(!context.viewState.hasChanges)
|
||||||
|
#expect(!context.viewState.isSearching)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
mutating func toggleUserOn() throws {
|
||||||
|
setup(mode: .moderator)
|
||||||
|
let firstUser = try #require(context.viewState.users.first { !context.viewState.isMemberSelected($0) },
|
||||||
|
"There should be a regular user available to promote.")
|
||||||
|
|
||||||
context.send(viewAction: .toggleMember(firstUser))
|
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,
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
//
|
|
||||||
// Copyright 2025 Element Creations Ltd.
|
|
||||||
// Copyright 2022-2025 New Vector Ltd.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
||||||
// Please see LICENSE files in the repository root for full details.
|
|
||||||
//
|
|
||||||
|
|
||||||
@testable import ElementX
|
|
||||||
import XCTest
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
class RoomDirectorySearchScreenScreenViewModelTests: XCTestCase { }
|
|
||||||
@@ -8,13 +8,14 @@
|
|||||||
|
|
||||||
@testable import ElementX
|
@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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
#expect(context.viewState.memberDetails == RoomMemberDetails(withProxy: roomMemberProxyMock))
|
||||||
|
#expect(context.ignoreUserAlert == nil)
|
||||||
|
#expect(context.alertInfo == nil)
|
||||||
|
}
|
||||||
|
|
||||||
XCTAssertEqual(context.viewState.memberDetails, RoomMemberDetails(withProxy: roomMemberProxyMock))
|
// MARK: - Helpers
|
||||||
XCTAssertNil(context.ignoreUserAlert)
|
|
||||||
XCTAssertNil(context.alertInfo)
|
private mutating func setup(roomMemberProxyMock: RoomMemberProxyMock, clientProxy: ClientProxyMock? = nil) {
|
||||||
|
self.roomMemberProxyMock = roomMemberProxyMock
|
||||||
|
roomProxyMock = JoinedRoomProxyMock(.init(name: ""))
|
||||||
|
roomProxyMock.getMemberUserIDClosure = { _ in
|
||||||
|
.success(roomMemberProxyMock)
|
||||||
|
}
|
||||||
|
// swiftlint:disable:next force_unwrapping
|
||||||
|
let userSession = clientProxy != nil ? UserSessionMock(.init(clientProxy: clientProxy!)) : UserSessionMock(.init())
|
||||||
|
viewModel = RoomMemberDetailsScreenViewModel(userID: roomMemberProxyMock.userID,
|
||||||
|
roomProxy: roomProxyMock,
|
||||||
|
userSession: userSession,
|
||||||
|
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||||
|
analytics: ServiceLocator.shared.analytics)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,22 +7,24 @@
|
|||||||
|
|
||||||
import Combine
|
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))
|
||||||
@@ -47,7 +51,7 @@ class RoomMembersFlowCoordinatorTests: XCTestCase {
|
|||||||
|
|
||||||
let clientProxy = ClientProxyMock(.init())
|
let clientProxy = ClientProxyMock(.init())
|
||||||
clientProxy.directRoomForUserIDReturnValue = .success(nil)
|
clientProxy.directRoomForUserIDReturnValue = .success(nil)
|
||||||
|
|
||||||
let flowParameters = CommonFlowParameters(userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
let flowParameters = CommonFlowParameters(userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
||||||
bugReportService: BugReportServiceMock(.init()),
|
bugReportService: BugReportServiceMock(.init()),
|
||||||
elementCallService: ElementCallServiceMock(.init()),
|
elementCallService: ElementCallServiceMock(.init()),
|
||||||
|
|||||||
@@ -8,44 +8,39 @@
|
|||||||
|
|
||||||
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] = [
|
||||||
.init(member: .init(withProxy: RoomMemberProxyMock.mockAdmin),
|
.init(member: .init(withProxy: RoomMemberProxyMock.mockAdmin),
|
||||||
verificationState: .notVerified),
|
verificationState: .notVerified),
|
||||||
@@ -56,247 +51,254 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
|
|||||||
.init(member: .init(withProxy: RoomMemberProxyMock.mockDan),
|
.init(member: .init(withProxy: RoomMemberProxyMock.mockDan),
|
||||||
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 {
|
||||||
|
setup(members: [.mockAlice, .mockBob])
|
||||||
|
|
||||||
context.searchQuery = "WWW"
|
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 }
|
|
||||||
guard let user = viewModel.state.visibleJoinedMembers.first(where: { $0.member.role == .user && $0.member.id != RoomMemberProxyMock.mockMe.userID })?.member else {
|
|
||||||
XCTFail("Expected to find a regular user.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
context.send(viewAction: .selectMember(user))
|
|
||||||
|
|
||||||
// Then the member's details should be shown.
|
|
||||||
try await deferred.fulfill()
|
|
||||||
XCTAssertNotNil(context.manageMemeberViewModel)
|
|
||||||
XCTAssertEqual(context.manageMemeberViewModel?.state.memberDetails.id, user.id)
|
|
||||||
XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canKick, false)
|
|
||||||
XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canBan, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testSelectUserAsAdmin() async throws {
|
|
||||||
// Given the room list viewed as an admin.
|
|
||||||
setup(with: .allMembersAsAdmin)
|
|
||||||
var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers }
|
|
||||||
try await deferred.fulfill()
|
|
||||||
XCTAssertNil(context.manageMemeberViewModel)
|
|
||||||
|
|
||||||
// When tapping on a user in the list.
|
|
||||||
deferred = deferFulfillment(context.$viewState) { $0.bindings.manageMemeberViewModel != nil }
|
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))
|
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)
|
|
||||||
|
#expect(context.manageMemeberViewModel == nil)
|
||||||
// When tapping on a moderator 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 {
|
||||||
|
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))
|
context.send(viewAction: .selectMember(admin))
|
||||||
|
|
||||||
// Then the administrator's details should be shown.
|
|
||||||
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))
|
context.send(viewAction: .selectMember(ownMember))
|
||||||
|
|
||||||
// Then your member's details should be shown.
|
|
||||||
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)
|
|
||||||
|
#expect(context.alertInfo == nil)
|
||||||
// When tapping on a banned member in the list.
|
|
||||||
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))
|
context.send(viewAction: .selectMember(bannedMember))
|
||||||
|
|
||||||
// Then an alert should be shown to unban the user.
|
|
||||||
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
|
||||||
try await deferred.fulfill()
|
try await deferred.fulfill()
|
||||||
|
|
||||||
deferred = deferFulfillment(context.$viewState) { $0.visibleBannedMembers.count == 0 && $0.bindings.mode == .members }
|
deferred = deferFulfillment(context.$viewState) { $0.visibleBannedMembers.count == 0 && $0.bindings.mode == .members }
|
||||||
subject.value = [RoomMemberProxyMock].allMembersAsAdmin
|
subject.value = [RoomMemberProxyMock].allMembersAsAdmin
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,73 +7,81 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
@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!
|
||||||
|
|
||||||
var context: RoomRolesAndPermissionsScreenViewModelType.Context {
|
var context: RoomRolesAndPermissionsScreenViewModelType.Context {
|
||||||
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(),
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,102 +9,96 @@
|
|||||||
@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")
|
||||||
let attributedStringBuilder = AttributedStringBuilder(mentionBuilder: MentionBuilder())
|
let attributedStringBuilder = AttributedStringBuilder(mentionBuilder: MentionBuilder())
|
||||||
let eventStringBuilder = RoomEventStringBuilder(stateEventStringBuilder: stateEventStringBuilder,
|
let eventStringBuilder = RoomEventStringBuilder(stateEventStringBuilder: stateEventStringBuilder,
|
||||||
@@ -112,13 +106,13 @@ final class RoomSummaryProviderTests: XCTestCase {
|
|||||||
destination: .roomList),
|
destination: .roomList),
|
||||||
shouldDisambiguateDisplayNames: true,
|
shouldDisambiguateDisplayNames: true,
|
||||||
shouldPrefixSenderName: true)
|
shouldPrefixSenderName: true)
|
||||||
|
|
||||||
roomSummaryProvider = RoomSummaryProvider(roomListService: RoomListServiceSDKMock(),
|
roomSummaryProvider = RoomSummaryProvider(roomListService: RoomListServiceSDKMock(),
|
||||||
eventStringBuilder: eventStringBuilder,
|
eventStringBuilder: eventStringBuilder,
|
||||||
name: "Test",
|
name: "Test",
|
||||||
notificationSettings: NotificationSettingsProxyMock(with: .init()),
|
notificationSettings: NotificationSettingsProxyMock(with: .init()),
|
||||||
appSettings: appSettings)
|
appSettings: appSettings)
|
||||||
|
|
||||||
dynamicEntriesController = RoomListDynamicEntriesControllerSDKMock()
|
dynamicEntriesController = RoomListDynamicEntriesControllerSDKMock()
|
||||||
dynamicEntriesController.setFilterKindReturnValue = true
|
dynamicEntriesController.setFilterKindReturnValue = true
|
||||||
let dynamicAdaptersResult = RoomListEntriesWithDynamicAdaptersResultSDKMock()
|
let dynamicAdaptersResult = RoomListEntriesWithDynamicAdaptersResultSDKMock()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
//
|
|
||||||
// Copyright 2025 Element Creations Ltd.
|
|
||||||
// Copyright 2022-2025 New Vector Ltd.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
||||||
// Please see LICENSE files in the repository root for full details.
|
|
||||||
//
|
|
||||||
|
|
||||||
@testable import ElementX
|
|
||||||
import XCTest
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
class SecureBackupKeyBackupScreenViewModelTests: XCTestCase { }
|
|
||||||
@@ -8,19 +8,20 @@
|
|||||||
|
|
||||||
import Combine
|
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)
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
//
|
|
||||||
// Copyright 2025 Element Creations Ltd.
|
|
||||||
// Copyright 2022-2025 New Vector Ltd.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
||||||
// Please see LICENSE files in the repository root for full details.
|
|
||||||
//
|
|
||||||
|
|
||||||
@testable import ElementX
|
|
||||||
import XCTest
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
class SecureBackupRecoveryKeyScreenViewModelTests: XCTestCase { }
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
//
|
|
||||||
// Copyright 2025 Element Creations Ltd.
|
|
||||||
// Copyright 2022-2025 New Vector Ltd.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
||||||
// Please see LICENSE files in the repository root for full details.
|
|
||||||
//
|
|
||||||
|
|
||||||
@testable import ElementX
|
|
||||||
import XCTest
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
class SecureBackupScreenViewModelTests: XCTestCase { }
|
|
||||||
@@ -7,35 +7,38 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
@testable import ElementX
|
@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.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user