diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 272b5dd01..adc778da4 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 77; + objectVersion = 63; objects = { /* Begin PBXAggregateTarget section */ @@ -63,6 +63,7 @@ 074F741578307EF0179EE47C /* MapTilerConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A1F2AAA3F0F2B72D2FFE4D0 /* MapTilerConfiguration.swift */; }; 07756D532EFE33DD1FA258E5 /* GeoURITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A7ED2EF5BDBAD2A7DBC4636 /* GeoURITests.swift */; }; 07CC13C5729C24255348CBBD /* ElementCallWidgetDriver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 309AD8BAE6437C31BA7157BF /* ElementCallWidgetDriver.swift */; }; + 083E1494D119BAF7A1121D49 /* WaitingConfirmation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07D8A2B48572142D0A52A9E0 /* WaitingConfirmation.swift */; }; 0847D85F823B55A3E95D16CC /* RoomPreviewProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3D94852AD5BB376CBCC3544 /* RoomPreviewProxy.swift */; }; 08547E55DD3686A84550996D /* SeparatorMediaEventsTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13F354AD441E2FD83DED89AF /* SeparatorMediaEventsTimelineView.swift */; }; 086D01E79C8E8D3F004FAF21 /* AudioPlayerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC9104846487244648D32C6D /* AudioPlayerProtocol.swift */; }; @@ -823,7 +824,6 @@ 8DF0EBD97753033C715D716E /* RoomFlowCoordinatorStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 407C8DD85179D2DB896FC0FA /* RoomFlowCoordinatorStateMachine.swift */; }; 8E650379587C31D7912ED67B /* UNNotification+Creator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0AEA686E425F86F6BA0404 /* UNNotification+Creator.swift */; }; 8E7A902CA16E24928F83646C /* ElementCallServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E321E840DCC63790049984F4 /* ElementCallServiceMock.swift */; }; - 8ECD4727BA96EF64DFCEC18F /* DeferredFulfillment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C39E32F0B876B962E418B5C2 /* DeferredFulfillment.swift */; }; 8ED8AF57A06F5EE9978ED23F /* AuthenticationStartScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FB89DC7F9A4A91020037001 /* AuthenticationStartScreenViewModelTests.swift */; }; 8F2FAA98457750D9D664136F /* LRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = 1081D3630AAD3ACEDDEC3A98 /* LRUCache */; }; 8F3AD08F2E706AA60F1A1D4D /* portrait_test_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = BC51BF90469412ABDE658CDD /* portrait_test_image.jpg */; }; @@ -1170,6 +1170,7 @@ CB9FB2BEF313072C705AC9B5 /* SecurityAndPrivacyScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0315C328FF40F84276364E66 /* SecurityAndPrivacyScreenViewModelTests.swift */; }; CBBBE597BE74A2DF68DE2209 /* NotificationItemProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DDB7A9BB466C56614BB589D /* NotificationItemProxyProtocol.swift */; }; CBD2ABE4C1A47ECD99E1488E /* NotificationSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421FA93BCC2840E66E4F306F /* NotificationSettingsScreenViewModelProtocol.swift */; }; + CBEE2AA8B0C9BDDB3FDD4C81 /* DeferredFulfillment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B91AD590B0B40718A0AA0C61 /* DeferredFulfillment.swift */; }; CC0D088F505F33A20DC5590F /* RoomStateEventStringBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEEAFB646E583655652C3D04 /* RoomStateEventStringBuilderTests.swift */; }; CC1C948F67A5510A340FD7F0 /* SessionDirectoriesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0825EAFD47332DD459DE893F /* SessionDirectoriesTests.swift */; }; CC961529F9F1854BEC3272C9 /* LayoutMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC8AA23D4F37CC26564F63C5 /* LayoutMocks.swift */; }; @@ -1578,7 +1579,7 @@ 044E501B8331B339874D1B96 /* CompoundIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompoundIcon.swift; sourceTree = ""; }; 045253F9967A535EE5B16691 /* Label.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Label.swift; sourceTree = ""; }; 046C0D3F53B0B5EF0A1F5BEA /* RoomSummaryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryTests.swift; sourceTree = ""; }; - 048A21188AB19349D026BECD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + 048A21188AB19349D026BECD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 04BB8DDE245ED86C489BA983 /* AccessibilityIdentifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityIdentifiers.swift; sourceTree = ""; }; 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = ""; }; 0516C69708D5CBDE1A8E77EC /* RoomDirectorySearchProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchProxyProtocol.swift; sourceTree = ""; }; @@ -1601,6 +1602,7 @@ 077D7C3BE199B6E5DDEC07EC /* AppCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorStateMachine.swift; sourceTree = ""; }; 07934EF08BB39353E4A94272 /* BlurEffectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurEffectView.swift; sourceTree = ""; }; 07C6B0B087FE6601C3F77816 /* JoinedRoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinedRoomProxy.swift; sourceTree = ""; }; + 07D8A2B48572142D0A52A9E0 /* WaitingConfirmation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitingConfirmation.swift; sourceTree = ""; }; 0825EAFD47332DD459DE893F /* SessionDirectoriesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDirectoriesTests.swift; sourceTree = ""; }; 08283301736A6FE9D558B2CB /* AppLockScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenViewModelProtocol.swift; sourceTree = ""; }; 0833F51229E166BCA141D004 /* RoomRolesAndPermissionsFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsFlowCoordinator.swift; sourceTree = ""; }; @@ -1662,7 +1664,7 @@ 128501375217576AF0FE3E92 /* RoomAttachmentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomAttachmentPicker.swift; sourceTree = ""; }; 12B09A94C519227264A41208 /* RoomMembershipDetailsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembershipDetailsProxy.swift; sourceTree = ""; }; 12FD5280AF55AB7F50F8E47D /* preview_avatar_room.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = preview_avatar_room.jpg; sourceTree = ""; }; - 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; path = IntegrationTests.xctestplan; sourceTree = ""; }; + 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = IntegrationTests.xctestplan; sourceTree = ""; }; 130ED565A078F7E0B59D9D25 /* UNTextInputNotificationResponse+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNTextInputNotificationResponse+Creator.swift"; sourceTree = ""; }; 136F80A613B55BDD071DCEA5 /* JoinRoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenModels.swift; sourceTree = ""; }; 13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -1683,7 +1685,7 @@ 16D09C79746BDCD9173EB3A7 /* RoomDetailsEditScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenModels.swift; sourceTree = ""; }; 16D353E10A64172D863769BF /* TombstonedAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TombstonedAvatarImage.swift; sourceTree = ""; }; 1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAnalyticsClient.swift; sourceTree = ""; }; - 174E4AEF3DED300AA81046EC /* compound-ios */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "compound-ios"; path = "compound-ios"; sourceTree = SOURCE_ROOT; }; + 174E4AEF3DED300AA81046EC /* compound-ios */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "compound-ios"; sourceTree = SOURCE_ROOT; }; 17A8AA0DFA06012A9DAB951E /* TimelineProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineProxyMock.swift; sourceTree = ""; }; 17BAE25A0E9E9F2F1BBA8930 /* DeactivateAccountScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeactivateAccountScreenViewModel.swift; sourceTree = ""; }; 181CF280BC8E3F335AFCB4B8 /* RemotePreferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemotePreferenceTests.swift; sourceTree = ""; }; @@ -1774,7 +1776,7 @@ 25E7E9B7FEAB6169D960C206 /* QRCodeLoginScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginScreenViewModelTests.swift; sourceTree = ""; }; 25F8664F1FB95AF3C4202478 /* PollFormScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenCoordinator.swift; sourceTree = ""; }; 260004737C573A56FA01E86E /* Encodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encodable.swift; sourceTree = ""; }; - 267BB1D5B08A9511F894CB57 /* PreviewTests.xctestplan */ = {isa = PBXFileReference; path = PreviewTests.xctestplan; sourceTree = ""; }; + 267BB1D5B08A9511F894CB57 /* PreviewTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = PreviewTests.xctestplan; sourceTree = ""; }; 26B0A96B8FE4849227945067 /* VoiceMessageRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecorder.swift; sourceTree = ""; }; 26EAAB54C6CE91D64B69A9F8 /* AppLockServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockServiceProtocol.swift; sourceTree = ""; }; 2711E5996016ABD6EAAEB58A /* LogLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogLevel.swift; sourceTree = ""; }; @@ -1856,7 +1858,7 @@ 358528B29FA72ACFD0D9644B /* SpacesScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpacesScreenCoordinator.swift; sourceTree = ""; }; 35A057BA9BE0F079784CD061 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 35AFCF4C05DEED04E3DB1A16 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; - 36DA824791172B9821EACBED /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + 36DA824791172B9821EACBED /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxyMock.swift; sourceTree = ""; }; 3747C96188856006F784BF49 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ko; path = ko.lproj/Localizable.stringsdict; sourceTree = ""; }; 37A63A59BFDDC494B1C20119 /* CallScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenViewModel.swift; sourceTree = ""; }; @@ -1974,7 +1976,7 @@ 4A2B5274C1D3D2999D643786 /* EncryptionResetPasswordScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreenViewModelProtocol.swift; sourceTree = ""; }; 4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockTimerTests.swift; sourceTree = ""; }; 4AB29A2D95D3469B5F016655 /* SecureBackupControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupControllerMock.swift; sourceTree = ""; }; - 4B1F71AC585827E6C416C15A /* AppIcon.icon */ = {isa = PBXFileReference; path = AppIcon.icon; sourceTree = ""; }; + 4B1F71AC585827E6C416C15A /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = ""; }; 4B2B564CA6570E1487A7C7CC /* SpaceRoomListProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceRoomListProxy.swift; sourceTree = ""; }; 4B2D4EEBE8C098BBADD10939 /* SecureBackupKeyBackupScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenCoordinator.swift; sourceTree = ""; }; 4B41FABA2B0AEF4389986495 /* LoginMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginMode.swift; sourceTree = ""; }; @@ -2320,7 +2322,7 @@ 8D55702474F279D910D2D162 /* RoomStateEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilder.swift; sourceTree = ""; }; 8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitorMock.swift; sourceTree = ""; }; - 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = ""; }; + 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = ""; }; 8E1584F8BCF407BB94F48F04 /* EncryptionResetPasswordScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreen.swift; sourceTree = ""; }; 8EAF4A49F3ACD8BB8B0D2371 /* ClientSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientSDKMock.swift; sourceTree = ""; }; 8F062DD2CCD95DC33528A16F /* KnockRequestProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockRequestProxy.swift; sourceTree = ""; }; @@ -2472,7 +2474,7 @@ AAD8234D0E9C9B12BF9F240B /* LocationAnnotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationAnnotation.swift; sourceTree = ""; }; AB07F03461023BC39C730922 /* PhishingDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhishingDetector.swift; sourceTree = ""; }; AB26D5444A4A7E095222DE8B /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.stringsdict"; sourceTree = ""; }; - AB389C38BD41EB3E47092CFB /* AccessibilityTests.xctestplan */ = {isa = PBXFileReference; path = AccessibilityTests.xctestplan; sourceTree = ""; }; + AB389C38BD41EB3E47092CFB /* AccessibilityTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AccessibilityTests.xctestplan; sourceTree = ""; }; ABA4CF2F5B4F68D02E412004 /* ServerConfirmationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenViewModelProtocol.swift; sourceTree = ""; }; AC0275CEE9CA078B34028BDF /* AppLockScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenViewModelTests.swift; sourceTree = ""; }; AC1DA29A5A041CC0BACA7CB0 /* MockImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockImageCache.swift; sourceTree = ""; }; @@ -2540,7 +2542,7 @@ B53AC78E49A297AC1D72A7CF /* AppMediator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMediator.swift; sourceTree = ""; }; B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableAvatarImage.swift; sourceTree = ""; }; B5D829FD8958376614504B18 /* TargetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetConfiguration.swift; sourceTree = ""; }; - B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; path = ConfettiScene.scn; sourceTree = ""; }; + B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = ConfettiScene.scn; sourceTree = ""; }; B6404166CBF5CC88673FF9E2 /* RoomDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetails.swift; sourceTree = ""; }; B65DDCF8E41759890355ACBC /* AuthenticationStartScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationStartScreenViewModelProtocol.swift; sourceTree = ""; }; B682FE2C44C5E163E7023B05 /* CopyTextButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyTextButton.swift; sourceTree = ""; }; @@ -2566,13 +2568,14 @@ B8A3B7637DDBD6AA97AC2545 /* CameraPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPicker.swift; sourceTree = ""; }; B8F28602AC7AC881AED37EBA /* NavigationCoordinators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCoordinators.swift; sourceTree = ""; }; B902EA6CD3296B0E10EE432B /* HomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreen.swift; sourceTree = ""; }; + B91AD590B0B40718A0AA0C61 /* DeferredFulfillment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredFulfillment.swift; sourceTree = ""; }; B99E13633862847D8B7E2815 /* StartChatScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenModels.swift; sourceTree = ""; }; BA241DEEF7C8A7181C0AEDC9 /* UserPreferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPreferenceTests.swift; sourceTree = ""; }; BA257D747DD7E6FFA5C2BE2D /* LinkNewDeviceServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkNewDeviceServiceMock.swift; sourceTree = ""; }; BA40B98B098B6F0371B750B3 /* TemplateScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenModels.swift; sourceTree = ""; }; BA919F521E9F0EE3638AFC85 /* BugReportScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreen.swift; sourceTree = ""; }; BB284643AF7AB131E307DCE0 /* AudioSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionProtocol.swift; sourceTree = ""; }; - BB576F4118C35E6B5124FA22 /* test_apple_image.heic */ = {isa = PBXFileReference; path = test_apple_image.heic; sourceTree = ""; }; + BB576F4118C35E6B5124FA22 /* test_apple_image.heic */ = {isa = PBXFileReference; lastKnownFileType = file; path = test_apple_image.heic; sourceTree = ""; }; BB5B00A014307CE37B2812CD /* TimelineViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewModelProtocol.swift; sourceTree = ""; }; BB6ED50FE104992419310EEB /* NotificationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationHandler.swift; sourceTree = ""; }; BB8BC4C791D0E88CFCF4E5DF /* ServerSelectionScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenCoordinator.swift; sourceTree = ""; }; @@ -2620,7 +2623,6 @@ C3285BD95B564CA2A948E511 /* OnboardingFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingFlowCoordinator.swift; sourceTree = ""; }; C33B3F17996DFDF5F0181512 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; C352359663A0E52BA20761EE /* LoadableImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableImage.swift; sourceTree = ""; }; - C39E32F0B876B962E418B5C2 /* DeferredFulfillment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredFulfillment.swift; sourceTree = ""; }; C4756240773D26AB74C22668 /* OrientationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrientationManagerProtocol.swift; sourceTree = ""; }; C4C1C19A4BE46EDE1411ECCE /* ThreadTimelineScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadTimelineScreenViewModelProtocol.swift; sourceTree = ""; }; C4C89820BB2B88D4EA28131C /* BugReportScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -2678,7 +2680,7 @@ CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProvider.swift; sourceTree = ""; }; CDE3F3911FF7CC639BDE5844 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; CEE20623EB4A9B88FB29F2BA /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/SAS.strings; sourceTree = ""; }; - CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; path = UnitTests.xctestplan; sourceTree = ""; }; + CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTests.xctestplan; sourceTree = ""; }; CF19027E7FFA5E63D148873A /* CreateRoomScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomScreenViewModel.swift; sourceTree = ""; }; CF847A34FC4C8C937CD39E08 /* LabsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabsScreenViewModelProtocol.swift; sourceTree = ""; }; CFFA5E881D281810AB428EA3 /* RoomPowerLevelsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPowerLevelsProxy.swift; sourceTree = ""; }; @@ -2747,7 +2749,7 @@ DC0AEA686E425F86F6BA0404 /* UNNotification+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNNotification+Creator.swift"; sourceTree = ""; }; DC10CCC8D68B863E20660DBC /* MessageForwardingScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModelProtocol.swift; sourceTree = ""; }; DC528B3764E3CF7FCFEF40E7 /* PollInteractionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollInteractionHandler.swift; sourceTree = ""; }; - DCA2D836BD10303F37FAAEED /* test_voice_message.m4a */ = {isa = PBXFileReference; path = test_voice_message.m4a; sourceTree = ""; }; + DCA2D836BD10303F37FAAEED /* test_voice_message.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = test_voice_message.m4a; sourceTree = ""; }; DCAC01A97A43BE07B9E94E43 /* ShareExtensionModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareExtensionModels.swift; sourceTree = ""; }; DCDAB580109C09A6AA97AF7E /* PollFormScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenTests.swift; sourceTree = ""; }; DCF239C619971FDE48132550 /* SecureBackupLogoutConfirmationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreenModels.swift; sourceTree = ""; }; @@ -2793,7 +2795,7 @@ E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = ""; }; E53BFB7E4F329621C844E8C3 /* AnalyticsPromptScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreen.swift; sourceTree = ""; }; E55B5EA766E89FF1F87C3ACB /* RoomNotificationSettingsProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsProxyProtocol.swift; sourceTree = ""; }; - E5E7D4EE7CA295E5039FDA21 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; path = portrait_test_video.mp4; sourceTree = ""; }; + E5E7D4EE7CA295E5039FDA21 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = portrait_test_video.mp4; sourceTree = ""; }; E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerProtocol.swift; sourceTree = ""; }; E5F2B6443D1ED8602F328539 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = ""; }; E5FDFAA04174CC99FB66391C /* EditRoomAddressScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditRoomAddressScreenViewModel.swift; sourceTree = ""; }; @@ -2841,7 +2843,7 @@ ED0CBEAB5F796BEFBAF7BB6A /* VideoRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineView.swift; sourceTree = ""; }; ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = ""; }; ED33988DA4FD4FC666800106 /* SessionVerificationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModel.swift; sourceTree = ""; }; - ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = ""; }; + ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = message.caf; sourceTree = ""; }; ED49073BB1C1FC649DAC2CCD /* LocationRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineView.swift; sourceTree = ""; }; ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreen.swift; sourceTree = ""; }; EDDE826EAB1BAB80C1104980 /* SpaceFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceFlowCoordinator.swift; sourceTree = ""; }; @@ -4544,6 +4546,15 @@ path = View; sourceTree = ""; }; + 638665D7260F974BA9A3BFA1 /* TestUtilities */ = { + isa = PBXGroup; + children = ( + B91AD590B0B40718A0AA0C61 /* DeferredFulfillment.swift */, + 07D8A2B48572142D0A52A9E0 /* WaitingConfirmation.swift */, + ); + path = TestUtilities; + sourceTree = ""; + }; 63E514D74481A3D9556DFFC3 /* SecureBackupLogoutConfirmationScreen */ = { isa = PBXGroup; children = ( @@ -4847,6 +4858,7 @@ A6AA0A048CAE428A5CA4CBBB /* LayoutTests */, 7583EAC171059A86B767209F /* MediaProvider */, 7DBC911559934065993A5FF4 /* NotificationManager */, + 638665D7260F974BA9A3BFA1 /* TestUtilities */, 1C62F5382CC9D9F7DCEC344A /* UserDiscoveryService */, ); path = Sources; @@ -5997,7 +6009,6 @@ AE52983FAFB4E0998C00EE8A /* CancellableTask.swift */, 127A57D053CE8C87B5EFB089 /* Consumable.swift */, 127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */, - C39E32F0B876B962E418B5C2 /* DeferredFulfillment.swift */, 7B25F959A434BB9923A3223F /* ExpiringTaskRunner.swift */, 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */, 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */, @@ -7222,7 +7233,6 @@ EE40B0E16A55BD23ECBFFD22 /* XCRemoteSwiftPackageReference "matrix-rich-text-editor-swift" */, C89CF7729E028671C5DC461E /* XCLocalSwiftPackageReference "compound-ios" */, ); - preferredProjectObjectVersion = 77; projectDirPath = ""; projectRoot = ""; targets = ( @@ -7623,7 +7633,7 @@ CD0088B763CD970CF1CBF8CB /* DateTests.swift in Sources */, 80F6C8EFCA4564B67F0D34B0 /* DeactivateAccountScreenViewModelTests.swift in Sources */, 34390DAE0C574DAD30CCA7D9 /* DeclineAndBlockScreenViewModelTests.swift in Sources */, - 8ECD4727BA96EF64DFCEC18F /* DeferredFulfillment.swift in Sources */, + CBEE2AA8B0C9BDDB3FDD4C81 /* DeferredFulfillment.swift in Sources */, A583B70939707197B0B21DFC /* DeferredFulfillmentTests.swift in Sources */, EDB6915EC953BB2A44AA608E /* EditRoomAddressScreenViewModelTests.swift in Sources */, D820B3C223E4C2E77BB2A2BF /* ElementCallServiceTests.swift in Sources */, @@ -7729,6 +7739,7 @@ 21AFEFB8CEFE56A3811A1F5B /* VoiceMessageCacheTests.swift in Sources */, 44BDD670FF9095ACE240A3A2 /* VoiceMessageMediaManagerTests.swift in Sources */, A3D7110C1E75E7B4A73BE71C /* VoiceMessageRecorderTests.swift in Sources */, + 083E1494D119BAF7A1121D49 /* WaitingConfirmation.swift in Sources */, 1443CEEE42491CF7CD8A146A /* XCTestCase.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -9133,9 +9144,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - OTHER_SWIFT_FLAGS = ( - "-DRELEASE", - ); + OTHER_SWIFT_FLAGS = "-DRELEASE"; PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.accessibility.tests"; PRODUCT_NAME = AccessibilityTests; SDKROOT = iphoneos; @@ -9154,9 +9163,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - OTHER_SWIFT_FLAGS = ( - "-DDEBUG", - ); + OTHER_SWIFT_FLAGS = "-DDEBUG"; PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.accessibility.tests"; PRODUCT_NAME = AccessibilityTests; SDKROOT = iphoneos; @@ -9178,9 +9185,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = "$(MARKETING_VERSION)"; - OTHER_SWIFT_FLAGS = ( - "-DIS_NSE", - ); + OTHER_SWIFT_FLAGS = "-DIS_NSE"; PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.nse"; PRODUCT_DISPLAY_NAME = "$(APP_DISPLAY_NAME)"; PRODUCT_NAME = NSE; @@ -9247,9 +9252,7 @@ "$(inherited)", "-ObjC", ); - OTHER_SWIFT_FLAGS = ( - "-DIS_MAIN_APP", - ); + OTHER_SWIFT_FLAGS = "-DIS_MAIN_APP"; PILLS_UT_TYPE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).pills"; PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(APP_NAME)"; @@ -9279,9 +9282,7 @@ "$(inherited)", "-ObjC", ); - OTHER_SWIFT_FLAGS = ( - "-DIS_MAIN_APP", - ); + OTHER_SWIFT_FLAGS = "-DIS_MAIN_APP"; PILLS_UT_TYPE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).pills"; PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(APP_NAME)"; @@ -9506,9 +9507,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - OTHER_SWIFT_FLAGS = ( - "-DDEBUG", - ); + OTHER_SWIFT_FLAGS = "-DDEBUG"; PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.ui.tests"; PRODUCT_NAME = UITests; SDKROOT = iphoneos; @@ -9527,9 +9526,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - OTHER_SWIFT_FLAGS = ( - "-DRELEASE", - ); + OTHER_SWIFT_FLAGS = "-DRELEASE"; PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.ui.tests"; PRODUCT_NAME = UITests; SDKROOT = iphoneos; @@ -9551,9 +9548,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = "$(MARKETING_VERSION)"; - OTHER_SWIFT_FLAGS = ( - "-DIS_NSE", - ); + OTHER_SWIFT_FLAGS = "-DIS_NSE"; PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.nse"; PRODUCT_DISPLAY_NAME = "$(APP_DISPLAY_NAME)"; PRODUCT_NAME = NSE; diff --git a/ElementX/SupportingFiles/target.yml b/ElementX/SupportingFiles/target.yml index 99629d8d6..5af6718b1 100644 --- a/ElementX/SupportingFiles/target.yml +++ b/ElementX/SupportingFiles/target.yml @@ -258,7 +258,6 @@ targets: - path: ../Sources excludes: - Other/Extensions/XCTestCase.swift - - Other/DeferredFulfillment.swift - Other/Extensions/XCUIElement.swift - path: ../../Secrets/Secrets.swift - path: ../Resources diff --git a/UnitTests/Sources/EmojiPickerScreenViewModelTests.swift b/UnitTests/Sources/EmojiPickerScreenViewModelTests.swift index d316c04d5..b00ea0bd9 100644 --- a/UnitTests/Sources/EmojiPickerScreenViewModelTests.swift +++ b/UnitTests/Sources/EmojiPickerScreenViewModelTests.swift @@ -25,27 +25,18 @@ struct EmojiPickerScreenViewModelTests { let reaction = "👋" let deferred = deferFulfillment(viewModel.actions) { $0 == .dismiss } - try await confirmation { confirmation in - var toggleReactionCalled = false + + try await waitForConfirmation(timeout: .seconds(5)) { confirmation in timelineProxy.toggleReactionToClosure = { toggledReaction, _ in - defer { - confirmation() - toggleReactionCalled = true - } + defer { confirmation() } #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() - } } + + try await deferred.fulfill() } // MARK: - Helpers diff --git a/UnitTests/Sources/NavigationTabCoordinatorTests.swift b/UnitTests/Sources/NavigationTabCoordinatorTests.swift index 5623deaf2..6077d5537 100644 --- a/UnitTests/Sources/NavigationTabCoordinatorTests.swift +++ b/UnitTests/Sources/NavigationTabCoordinatorTests.swift @@ -168,17 +168,17 @@ struct NavigationTabCoordinatorTests { } @Test - mutating func overlayDismissalCallbackWhenChangingMode() async throws { + mutating func overlayDismissalCallbackWhenChangingMode() async { let overlayCoordinator = SomeTestCoordinator() - try await confirmation("Callback should not be called when just changing mode", - expectedCount: 0) { confirmation in + await waitForConfirmation("Callback should not be called when just changing mode", + expectedCount: 0, + timeout: .seconds(1)) { confirmation in navigationTabCoordinator.setOverlayCoordinator(overlayCoordinator) { confirmation() } navigationTabCoordinator.setOverlayPresentationMode(.minimized) - try await Task.sleep(for: .seconds(1)) } } diff --git a/UnitTests/Sources/PollFormScreenViewModelTests.swift b/UnitTests/Sources/PollFormScreenViewModelTests.swift index ded37d477..aa96557d9 100644 --- a/UnitTests/Sources/PollFormScreenViewModelTests.swift +++ b/UnitTests/Sources/PollFormScreenViewModelTests.swift @@ -162,26 +162,17 @@ struct PollFormScreenViewModelTests { let deferred = deferFulfillment(viewModel.actions) { $0 == .close } - try await confirmation { confirmation in - var redactReasonCalled = false + await waitForConfirmation(timeout: .seconds(1)) { confirmation in 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() - } } + try await deferred.fulfill() } // MARK: - Helpers diff --git a/ElementX/Sources/Other/DeferredFulfillment.swift b/UnitTests/Sources/TestUtilities/DeferredFulfillment.swift similarity index 98% rename from ElementX/Sources/Other/DeferredFulfillment.swift rename to UnitTests/Sources/TestUtilities/DeferredFulfillment.swift index 23318dcfa..7f8320673 100644 --- a/ElementX/Sources/Other/DeferredFulfillment.swift +++ b/UnitTests/Sources/TestUtilities/DeferredFulfillment.swift @@ -10,7 +10,11 @@ import Combine import Testing struct DeferredFulfillment { - let closure: () async throws -> T + private let closure: () async throws -> T + + fileprivate init(_ closure: @escaping () async throws -> T) { + self.closure = closure + } @discardableResult func fulfill() async throws -> T { @@ -18,7 +22,7 @@ struct DeferredFulfillment { } } -struct DeferredFulfillmentError: Error { +private struct DeferredFulfillmentError: Error { static func noOutput(message: String?, sourceLocation: SourceLocation) -> Self { defer { Issue.record(Comment(rawValue: message ?? "No Output"), sourceLocation: sourceLocation) } return .init() diff --git a/UnitTests/Sources/TestUtilities/WaitingConfirmation.swift b/UnitTests/Sources/TestUtilities/WaitingConfirmation.swift new file mode 100644 index 000000000..829f9b68b --- /dev/null +++ b/UnitTests/Sources/TestUtilities/WaitingConfirmation.swift @@ -0,0 +1,206 @@ +// +// Copyright 2026 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import Synchronization +import Testing + +/// A class that provides a mechanism to confirm that a specific action or event +/// has occurred a given number of times within an async context. +/// +/// `WaitingConfirmation` is used in conjunction with ``waitForConfirmation(_:expectedCount:isolation:sourceLocation:_:)`` +/// to synchronize async test expectations. It bridges between your test code and +/// Swift Testing's `confirmation` mechanism using an `AsyncStream` under the hood. +/// +/// You typically interact with this type via its `callAsFunction()` sugar: +/// ```swift +/// await waitForConfirmation { confirmation in +/// sut.onEvent = { confirmation() } +/// sut.triggerEvent() +/// } +/// ``` +final class WaitingConfirmation: Sendable { + private let continuation: AsyncStream.Continuation + private let expectedCount: Int + private let confirmationsCount: Mutex + + fileprivate init(continuation: AsyncStream.Continuation, expectedCount: Int) { + self.continuation = continuation + self.expectedCount = expectedCount + confirmationsCount = .init(0) + } + + /// Confirms that the expected event has occurred once. + /// + /// Each call yields a value into the underlying stream, incrementing the confirmation count. + /// When the count reaches `expectedCount`, the stream is finished, unblocking ``waitForConfirmation``. + /// + /// This method is thread-safe — the count increment and the finish check are performed + /// atomically inside a `Mutex` lock. + func confirm() { + confirmationsCount.withLock { value in + continuation.yield() + value += 1 + if value == expectedCount { + continuation.finish() + } + } + } + + /// Allows the instance to be called directly as a function, forwarding to ``confirm()``. + /// + /// This enables the ergonomic shorthand `confirmation()` instead of `confirmation.confirm()`. + func callAsFunction() { + confirm() + } +} + +/// Waits for a confirmation to be triggered an expected number of times within a synchronous body. +/// +/// This is a wrapper around Swift Testing's `confirmation` that removes the need to manually +/// manage an `AsyncStream` at the call site. The body receives a ``WaitingConfirmation`` instance +/// which can be called directly to signal that the expected event occurred. +/// +/// The body is synchronous by design — it is intended for setting up mocks and triggering +/// actions that schedule async work, rather than performing async work itself. The async +/// waiting happens internally once the body returns, by draining the stream until all +/// confirmations are received. +/// +/// Unlike the timeout variant, this overload does not escape the body closure, which means +/// you can safely capture mutable structs — a common pattern in Swift Testing. +/// +/// > **Warning**: This overload has no timeout. If ``WaitingConfirmation/confirm()`` is never called, +/// > the test will hang indefinitely. Prefer the timeout variant when the confirmation +/// > depends on asynchronous work that could silently fail. +/// +/// Example: +/// ```swift +/// await waitForConfirmation(expectedCount: 2) { confirmation in +/// sut.onEvent = { +/// confirmation() +/// } +/// sut.triggerEvent() +/// sut.triggerEvent() +/// } +/// ``` +/// +/// - Parameters: +/// - comment: An optional comment to attach to the confirmation for test reporting. +/// - expectedCount: The number of times ``WaitingConfirmation/confirm()`` must be called. +/// Must be greater than 0, otherwise a test failure is recorded and execution stops. +/// Defaults to `1`. +/// - isolation: The actor isolation context. Defaults to the caller's isolation via `#isolation`. +/// - sourceLocation: The source location for failure reporting. Defaults to the call site via `#_sourceLocation`. +/// - body: A synchronous closure receiving a ``WaitingConfirmation`` instance used to signal +/// event occurrences. The closure may throw, and any thrown errors are rethrown to the caller. +/// Typically used to configure mocks and trigger the action under test. +/// - Returns: The value returned by `body`. +func waitForConfirmation(_ comment: Comment? = nil, + expectedCount: Int = 1, + isolation: isolated (any Actor)? = #isolation, + sourceLocation: SourceLocation = #_sourceLocation, + _ body: (WaitingConfirmation) throws -> sending R) async rethrows -> R { + guard expectedCount > 0 else { + // Or may run indefinitely + Issue.record("Expected count must be greater than 0", sourceLocation: sourceLocation) + preconditionFailure() + } + + let (stream, continuation) = AsyncStream.makeStream(of: Void.self) + return try await confirmation(comment, + expectedCount: expectedCount, + isolation: isolation, + sourceLocation: sourceLocation) { confirmation in + let result = try body(.init(continuation: continuation, + expectedCount: expectedCount)) + for await _ in stream { + confirmation() + } + return result + } +} + +/// Waits for a confirmation to be triggered an expected number of times within a synchronous body, +/// with a timeout. +/// +/// This overload behaves like ``waitForConfirmation(_:expectedCount:isolation:sourceLocation:_:)`` +/// but races the stream against a timeout. If the timeout expires before all confirmations +/// are received, the stream is forcefully finished and Swift Testing records whatever +/// confirmations were received up to that point — which will cause a test failure if +/// `expectedCount` was not reached. +/// +/// The body is synchronous by design — it is intended for setting up mocks and triggering +/// actions that schedule async work, rather than performing async work itself. The async +/// waiting and timeout racing happen internally once the body returns. +/// +/// > Note: Because this overload uses `withTaskGroup` internally to race the stream against +/// > the timeout, the `body` closure is implicitly `@escaping`. This is why this is a separate +/// > overload rather than a single function with an optional timeout — keeping them separate +/// > allows the non-timeout variant to avoid `@escaping`, which lets you capture mutable structs +/// > in `body` as is common in Swift Testing. +/// +/// Example: +/// ```swift +/// await waitForConfirmation(expectedCount: 1, timeout: .seconds(2)) { confirmation in +/// sut.onNetworkResponse = { confirmation() } +/// sut.startRequest() +/// } +/// ``` +/// +/// - Parameters: +/// - comment: An optional comment to attach to the confirmation for test reporting. +/// - expectedCount: The number of times ``WaitingConfirmation/confirm()`` must be called. +/// Must be equal to or greater than 0, otherwise a test failure is recorded +/// and execution stops. Defaults to `1`. +/// Pass `0` to assert that the event never fires within the timeout window — +/// useful for verifying that a function does NOT trigger under specific conditions. +/// - timeout: The maximum duration to wait for all confirmations before finishing the stream. +/// - isolation: The actor isolation context. Defaults to the caller's isolation via `#isolation`. +/// - sourceLocation: The source location for failure reporting. Defaults to the call site via `#_sourceLocation`. +/// - body: A synchronous closure receiving a ``WaitingConfirmation`` instance used to signal +/// event occurrences. The closure may throw, and any thrown errors are rethrown to the caller. +/// Typically used to configure mocks and trigger the action under test. +/// - Returns: The value returned by `body`. +func waitForConfirmation(_ comment: Comment? = nil, + expectedCount: Int = 1, + timeout: Duration, + isolation: isolated (any Actor)? = #isolation, + sourceLocation: SourceLocation = #_sourceLocation, + _ body: (WaitingConfirmation) throws -> sending R) async rethrows -> R { + guard expectedCount >= 0 else { + // Or may run indefinitely + Issue.record("Expected count must be equal or greater than 0", sourceLocation: sourceLocation) + preconditionFailure() + } + + let (stream, continuation) = AsyncStream.makeStream(of: Void.self) + return try await confirmation(comment, + expectedCount: expectedCount, + isolation: isolation, + sourceLocation: sourceLocation) { confirmation in + let result = try body(.init(continuation: continuation, + expectedCount: expectedCount)) + + // The reason why I don't add to the task group directly the non timeout implementation + // is that I do not want the body to be marked as @escaping and thus to be able to capture + // even mutable structs which is common in Swift Testing. + await withTaskGroup(of: Void.self) { group in + group.addTask { + for await _ in stream { + confirmation() + } + } + group.addTask { + try? await Task.sleep(for: timeout) + continuation.finish() + } + await group.next() + group.cancelAll() + } + + return result + } +} diff --git a/UnitTests/SupportingFiles/target.yml b/UnitTests/SupportingFiles/target.yml index 8f4f448c1..b6f254a30 100644 --- a/UnitTests/SupportingFiles/target.yml +++ b/UnitTests/SupportingFiles/target.yml @@ -52,7 +52,6 @@ targets: - path: ../../DevelopmentAssets - path: ../../ElementX/Sources/Other/Extensions/Publisher.swift - path: ../../ElementX/Sources/Other/Extensions/XCTestCase.swift - - path: ../../ElementX/Sources/Other/DeferredFulfillment.swift - path: ../../ElementX/Sources/Other/InfoPlistReader.swift - path: ../../Tools/Scripts/Templates/SimpleScreenExample/Tests/Unit