From 0dc0bea945a0c81fa643ea47c8ff1791f15e3171 Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 7 Jul 2023 15:08:32 +0100 Subject: [PATCH] Add xcDeferFulfillment to help with tests that have publishers. (#1278) * Add xcAwaitDeferred to aid with tests that have publishers. * Improve naming * Address Comments - Fix naming - Add optional expectation message - Use trailing closure syntax --- ElementX.xcodeproj/project.pbxproj | 18 +++--- .../SwiftUI/Views/RoundedCornerShape.swift | 5 -- .../ReportContentScreenModels.swift | 4 +- .../Services/Room/RoomProxyProtocol.swift | 2 +- UnitTests/Sources/Extensions/XCTestCase.swift | 63 +++++++++++++++++++ .../Sources/HomeScreenViewModelTests.swift | 3 +- .../Sources/ReportContentViewModelTests.swift | 12 ++-- .../Sources/RoomScreenViewModelTests.swift | 9 ++- 8 files changed, 92 insertions(+), 24 deletions(-) create mode 100644 UnitTests/Sources/Extensions/XCTestCase.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 67871053d..b293b6ff7 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -267,6 +267,7 @@ 659E5B766F76FDEC1BF393A4 /* RoomDetailsEditScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E413F4CBD7BF0588F394A9DD /* RoomDetailsEditScreenViewModel.swift */; }; 65EDA77363BEDC40CDE43B43 /* InvitesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42ADEA322D2089391E049535 /* InvitesScreen.swift */; }; 663E198678778F7426A9B27D /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FAFE1C2149E6AC8156ED2B /* Collection.swift */; }; + 664F77F02A57617A00FB9B24 /* XCTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664F77EF2A57617A00FB9B24 /* XCTestCase.swift */; }; 6713835120D94BAA8ED7E3E5 /* MessageForwardingScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59846FA04E1DBBFDD8829C2A /* MessageForwardingScreenUITests.swift */; }; 67160204A8D362BB7D4AD259 /* Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693E16574C6F7F9FA1015A8C /* Search.swift */; }; 67C05C50AD734283374605E3 /* MatrixEntityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */; }; @@ -835,7 +836,7 @@ 127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentValuePublisher.swift; sourceTree = ""; }; 12EDAFB64FA5F6812D54F39A /* MigrationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationScreenViewModel.swift; sourceTree = ""; }; 12F1E7F9C2BE8BB751037826 /* WaitlistScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenCoordinator.swift; 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 = ""; }; 13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; 1423AB065857FA546444DB15 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; @@ -970,7 +971,7 @@ 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxyProtocol.swift; sourceTree = ""; }; 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineView.swift; sourceTree = ""; }; 47873756E45B46683D97DC32 /* LegalInformationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenModels.swift; sourceTree = ""; }; - 478BE8591BD13E908EF70C0C /* DesignKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = DesignKit; path = DesignKit; sourceTree = SOURCE_ROOT; }; + 478BE8591BD13E908EF70C0C /* DesignKit */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DesignKit; sourceTree = SOURCE_ROOT; }; 4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramePreferenceKey.swift; sourceTree = ""; }; 47EBB5D698CE9A25BB553A2D /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; 47F29139BC2A804CE5E0757E /* MediaUploadPreviewScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModel.swift; sourceTree = ""; }; @@ -1047,6 +1048,7 @@ 653610CB5F9776EAAAB98155 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fr; path = fr.lproj/Localizable.stringsdict; sourceTree = ""; }; 65AAD845E53B0C8B5E0812C2 /* UserDiscoveryService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoveryService.swift; sourceTree = ""; }; 65C2B80DD0BF6F10BB5FA922 /* MockAuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthenticationServiceProxy.swift; sourceTree = ""; }; + 664F77EF2A57617A00FB9B24 /* XCTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestCase.swift; sourceTree = ""; }; 6654859746B0BE9611459391 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = ""; }; 667DD3A9D932D7D9EB380CAA /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sk; path = sk.lproj/Localizable.stringsdict; sourceTree = ""; }; 669F35C505ACE1110589F875 /* MediaUploadingPreprocessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadingPreprocessor.swift; sourceTree = ""; }; @@ -1143,7 +1145,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 = ""; }; 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = ""; }; - 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = ""; }; + 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = ""; }; 8E1BBA73B611EDEEA6E20E05 /* InvitesScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenModels.swift; sourceTree = ""; }; 8EC57A32ABC80D774CC663DB /* SettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenUITests.swift; sourceTree = ""; }; 8F21ED7205048668BEB44A38 /* AppActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppActivityView.swift; sourceTree = ""; }; @@ -1249,7 +1251,7 @@ B4CFE236419E830E8946639C /* Analytics+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Analytics+SwiftUI.swift"; sourceTree = ""; }; B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableAvatarImage.swift; sourceTree = ""; }; B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = ""; }; - B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; path = ConfettiScene.scn; sourceTree = ""; }; + B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = ConfettiScene.scn; sourceTree = ""; }; B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkerRoomTimelineView.swift; sourceTree = ""; }; B697816AF93DA06EC58C5D70 /* WaitlistScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenViewModelProtocol.swift; sourceTree = ""; }; B6E89E530A8E92EC44301CA1 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; @@ -1327,7 +1329,7 @@ CD6B0C4639E066915B5E6463 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProvider.swift; sourceTree = ""; }; CEE0E6043EFCF6FD2A341861 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = ""; }; - CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; path = UnitTests.xctestplan; sourceTree = ""; }; + CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTests.xctestplan; sourceTree = ""; }; CF48AF076424DBC1615C74AD /* AuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProxy.swift; sourceTree = ""; }; D0140615D2232612C813FD6C /* EncryptedHistoryRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedHistoryRoomTimelineItem.swift; sourceTree = ""; }; D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoverySection.swift; sourceTree = ""; }; @@ -1397,7 +1399,7 @@ ECF79FB25E2D4BD6F50CE7C9 /* RoomMembersListScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModel.swift; sourceTree = ""; }; ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomCell.swift; sourceTree = ""; }; ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = ""; }; - ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = ""; }; + ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = message.caf; sourceTree = ""; }; ED983D4DCA5AFA6E1ED96099 /* StateRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRoomTimelineView.swift; sourceTree = ""; }; EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelTests.swift; sourceTree = ""; }; EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = ""; }; @@ -2353,6 +2355,7 @@ children = ( 60F18AECC9D38C2B6D85F99C /* Publisher.swift */, 818CBE6249ED6E8FC30E8366 /* ViewModelContext.swift */, + 664F77EF2A57617A00FB9B24 /* XCTestCase.swift */, ); path = Extensions; sourceTree = ""; @@ -3993,6 +3996,7 @@ EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */, 8AC256AF0EC54658321C9241 /* LegalInformationScreenViewModelTests.swift in Sources */, 0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */, + 664F77F02A57617A00FB9B24 /* XCTestCase.swift in Sources */, 149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */, 1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */, 2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */, diff --git a/ElementX/Sources/Other/SwiftUI/Views/RoundedCornerShape.swift b/ElementX/Sources/Other/SwiftUI/Views/RoundedCornerShape.swift index 2f7784e14..fe7de9841 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/RoundedCornerShape.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/RoundedCornerShape.swift @@ -20,11 +20,6 @@ struct RoundedCornerShape: Shape { let radius: CGFloat let corners: UIRectCorner - init(radius: CGFloat, corners: UIRectCorner) { - self.radius = radius - self.corners = corners - } - func path(in rect: CGRect) -> Path { var path = Path() diff --git a/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenModels.swift b/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenModels.swift index 5678348c5..784747963 100644 --- a/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenModels.swift +++ b/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenModels.swift @@ -16,11 +16,11 @@ import Foundation -enum ReportContentScreenViewModelAction { +enum ReportContentScreenViewModelAction: Equatable { case cancel case submitStarted case submitFinished - case submitFailed(error: Error) + case submitFailed(error: RoomProxyError) } struct ReportContentScreenViewState: BindableState { diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index d321e8baf..9e8f16654 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -18,7 +18,7 @@ import Combine import Foundation import MatrixRustSDK -enum RoomProxyError: Error { +enum RoomProxyError: Error, Equatable { case noMoreMessagesToBackPaginate case failedPaginatingBackwards case failedRetrievingMemberAvatarURL diff --git a/UnitTests/Sources/Extensions/XCTestCase.swift b/UnitTests/Sources/Extensions/XCTestCase.swift new file mode 100644 index 000000000..58604eced --- /dev/null +++ b/UnitTests/Sources/Extensions/XCTestCase.swift @@ -0,0 +1,63 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import XCTest + +extension XCTestCase { + /// XCTest utility that assists in subscribing to a publisher and deferring the fulfilment and results until some other actions have been performed. + /// + /// ``` + /// let collectedEvents = somePublisher.collect(3).first() + /// let awaitDeferred = deferFulfillment(collectedEvents) + /// // Do some other work that publishes to somePublisher + /// XCTAssertEqual(try await awaitDeferred.execute(), [expected, values, here]) + /// ``` + /// - Parameters: + /// - publisher: The publisher to wait on. + /// - 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(_ publisher: T, timeout: TimeInterval = 10, message: String? = nil) -> DeferredFulfillment { + var result: Result? + let expectation = expectation(description: message ?? "Awaiting publisher") + let cancellable = publisher + .sink { completion in + switch completion { + case .failure(let error): + result = .failure(error) + case .finished: + break + } + expectation.fulfill() + } receiveValue: { value in + result = .success(value) + } + + return DeferredFulfillment { + await self.fulfillment(of: [expectation], timeout: timeout) + cancellable.cancel() + let unwrappedResult = try XCTUnwrap(result, "Awaited publisher did not produce any output") + return try unwrappedResult.get() + } + } + + struct DeferredFulfillment { + let closure: () async throws -> T + @discardableResult func fulfill() async throws -> T { + try await closure() + } + } +} diff --git a/UnitTests/Sources/HomeScreenViewModelTests.swift b/UnitTests/Sources/HomeScreenViewModelTests.swift index 5c59f32ff..5f6fd8085 100644 --- a/UnitTests/Sources/HomeScreenViewModelTests.swift +++ b/UnitTests/Sources/HomeScreenViewModelTests.swift @@ -87,8 +87,9 @@ class HomeScreenViewModelTests: XCTestCase { let room: RoomProxyMock = .init(with: .init(id: mockRoomId, displayName: "Some room")) room.leaveRoomClosure = { .failure(.failedLeavingRoom) } clientProxy.roomForIdentifierMocks[mockRoomId] = room + let deferred = deferFulfillment(context.$viewState.first(), message: "viewState should be published.") context.send(viewAction: .confirmLeaveRoom(roomIdentifier: mockRoomId)) - await context.nextViewState() + try await deferred.fulfill() XCTAssertNotNil(context.alertInfo) } diff --git a/UnitTests/Sources/ReportContentViewModelTests.swift b/UnitTests/Sources/ReportContentViewModelTests.swift index 66fbc9b7c..11985f27e 100644 --- a/UnitTests/Sources/ReportContentViewModelTests.swift +++ b/UnitTests/Sources/ReportContentViewModelTests.swift @@ -14,9 +14,8 @@ // limitations under the License. // -import XCTest - @testable import ElementX +import XCTest @MainActor class ReportContentScreenViewModelTests: XCTestCase { @@ -24,7 +23,7 @@ class ReportContentScreenViewModelTests: XCTestCase { let senderID = "@meany:server.com" let reportReason = "I don't like it." - func testReportContent() async { + func testReportContent() async throws { // Given the report content view for some content. let roomProxy = RoomProxyMock(with: .init(displayName: "test")) roomProxy.reportContentReasonReturnValue = .success(()) @@ -32,13 +31,16 @@ class ReportContentScreenViewModelTests: XCTestCase { senderID: senderID, roomProxy: roomProxy) + let deferred = deferFulfillment(viewModel.actions.collect(2).first(), message: "2 actions should be published.") + // When reporting the content without ignoring the user. viewModel.state.bindings.reasonText = reportReason viewModel.state.bindings.ignoreUser = false viewModel.context.send(viewAction: .submit) - _ = await viewModel.actions.values.first() - + let actions = try await deferred.fulfill() + XCTAssertEqual(actions, [.submitStarted, .submitFinished]) + // Then the content should be reported, but the user should not be included. XCTAssertEqual(roomProxy.reportContentReasonCallsCount, 1, "The content should always be reported.") XCTAssertEqual(roomProxy.reportContentReasonReceivedArguments?.eventID, itemID, "The event ID should match the content being reported.") diff --git a/UnitTests/Sources/RoomScreenViewModelTests.swift b/UnitTests/Sources/RoomScreenViewModelTests.swift index 222643b69..ccb8eeba8 100644 --- a/UnitTests/Sources/RoomScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomScreenViewModelTests.swift @@ -213,9 +213,10 @@ class RoomScreenViewModelTests: XCTestCase { let roomProxyMock = RoomProxyMock(with: .init(displayName: "")) let roomMemberMock = RoomMemberProxyMock() roomMemberMock.userID = "bob" + let expectation = XCTestExpectation(description: "Go to user details") + roomProxyMock.getMemberUserIDClosure = { _ in - try? await Task.sleep(for: .milliseconds(200)) - return .success(roomMemberMock) + .success(roomMemberMock) } let viewModel = RoomScreenViewModel(timelineController: timelineController, @@ -229,6 +230,7 @@ class RoomScreenViewModelTests: XCTestCase { switch action { case .displayRoomMemberDetails(let member): XCTAssert(member === roomMemberMock) + expectation.fulfill() default: XCTFail("Did not received the expected action") } @@ -236,7 +238,8 @@ class RoomScreenViewModelTests: XCTestCase { // Test viewModel.context.send(viewAction: .tappedOnUser(userID: "bob")) - try? await Task.sleep(for: .milliseconds(300)) + await fulfillment(of: [expectation]) + XCTAssert(userIndicatorControllerMock.submitIndicatorDelayCallsCount == 1) XCTAssert(userIndicatorControllerMock.retractIndicatorWithIdCallsCount == 1) XCTAssert(roomProxyMock.getMemberUserIDCallsCount == 1)