From 7eda122d20687f1b00af2ccac6bfe5f0715173ec Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 26 Sep 2023 13:28:29 +0300 Subject: [PATCH] Fix various flakey unit tests (#1783) * Fix flakey emoji provider tests * Fix flakey RoomScreenViewModel tests * Fix flakey HomeScreenViewModel tests * Fix flakey RoomMemberListScreen tests, problem with bindings getting overriden and deferFulfillment cancellable not getting stored * Fix flakey RoomNotificationSettingsScreen tests and crashes * Fix flakey RoomMemberDetailsScreen tests * Deprecate old `deferFulfillment` and `nextViewState` methods * Convert more files to the new `deferFulfillment` * Converted the rest of the tests to the new deferFulfillment * Removed now unused `nextViewState` and `deferFulfillment` * Remove automatic retries from unit tests * Reset analytics flag after running unit tests * Address PR comments * Introduce a new `deferFulfillment(publisher, keyPath, transitionValues)` method and use it where appropiate --- ElementX.xcodeproj/project.pbxproj | 4 - .../EmojiPickerScreenViewModel.swift | 4 +- .../RoomMembersListScreenModels.swift | 2 +- .../RoomMembersListScreenViewModel.swift | 5 +- .../Services/Emojis/EmojiProvider.swift | 4 +- ...nalyticsSettingsScreenViewModelTests.swift | 4 + .../Sources/BugReportViewModelTests.swift | 43 +-- .../CreatePollScreenViewModelTests.swift | 11 +- UnitTests/Sources/EmojiProviderTests.swift | 54 ++-- .../Sources/Extensions/ViewModelContext.swift | 24 -- UnitTests/Sources/Extensions/XCTestCase.swift | 53 +++- .../Sources/HomeScreenViewModelTests.swift | 20 +- .../Sources/InviteUsersViewModelTests.swift | 33 ++- .../Sources/InvitesScreenViewModelTests.swift | 8 +- ...tionSettingsEditScreenViewModelTests.swift | 152 +++++----- ...ficationSettingsScreenViewModelTests.swift | 274 +++++++++++------- .../Sources/ReportContentViewModelTests.swift | 18 +- .../RoomDetailsEditScreenViewModelTests.swift | 6 +- .../Sources/RoomDetailsViewModelTests.swift | 207 ++++++++----- .../Sources/RoomFlowCoordinatorTests.swift | 23 +- .../RoomMemberDetailsViewModelTests.swift | 60 ++-- .../RoomMembersListViewModelTests.swift | 62 +++- ...ficationSettingsScreenViewModelTests.swift | 207 +++++++------ .../Sources/RoomScreenViewModelTests.swift | 53 ++-- .../SessionVerificationViewModelTests.swift | 6 +- .../StaticLocationScreenViewModelTests.swift | 22 +- fastlane/Fastfile | 1 - 27 files changed, 841 insertions(+), 519 deletions(-) delete mode 100644 UnitTests/Sources/Extensions/ViewModelContext.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 90de66d1c..14a71cf10 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -485,7 +485,6 @@ 992F5E750F5030C4BA2D0D03 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 01C4C7DB37597D7D8379511A /* Assets.xcassets */; }; 9965CB800CE6BC74ACA969FC /* EncryptedHistoryRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75697AB5E64A12F1F069F511 /* EncryptedHistoryRoomTimelineView.swift */; }; 99ED42B8F8D6BFB1DBCF4C45 /* AnalyticsEvents in Frameworks */ = {isa = PBXBuildFile; productRef = D661CAB418C075A94306A792 /* AnalyticsEvents */; }; - 99F8DA4CCC6772EE5FE68E24 /* ViewModelContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 818CBE6249ED6E8FC30E8366 /* ViewModelContext.swift */; }; 9A3B0CDF097E3838FB1B9595 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; }; 9A4E3D5AA44B041DAC3A0D81 /* OIDCAuthenticationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92390F9FA98255440A6BF5F8 /* OIDCAuthenticationPresenter.swift */; }; 9AC5F8142413862A9E3A2D98 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = A7CA6F33C553805035C3B114 /* DeviceKit */; }; @@ -1259,7 +1258,6 @@ 80C4927D09099497233E9980 /* WaitlistScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreen.swift; sourceTree = ""; }; 80E815FF3CC5E5A355E3A25E /* RoomMessageEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageEventStringBuilder.swift; sourceTree = ""; }; 818695BED971753243FEF897 /* StickerRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerRoomTimelineItem.swift; sourceTree = ""; }; - 818CBE6249ED6E8FC30E8366 /* ViewModelContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelContext.swift; sourceTree = ""; }; 8196D64EB9CF2AF1F43E4ED1 /* AnalyticsPromptScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreenViewModelProtocol.swift; sourceTree = ""; }; 81A9B5225D0881CEFA2CF7C9 /* RoomNotificationSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenViewModel.swift; sourceTree = ""; }; 81B17B1F29448D1B9049B11C /* ReportContentScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenViewModel.swift; sourceTree = ""; }; @@ -2647,7 +2645,6 @@ isa = PBXGroup; children = ( 60F18AECC9D38C2B6D85F99C /* Publisher.swift */, - 818CBE6249ED6E8FC30E8366 /* ViewModelContext.swift */, 74611A4182DCF5F4D42696EC /* XCTestCase.swift */, ); path = Extensions; @@ -4525,7 +4522,6 @@ A1DF0E1E526A981ED6D5DF44 /* UserIndicatorControllerTests.swift in Sources */, 04F17DE71A50206336749BAC /* UserPreferenceTests.swift in Sources */, 81A7C020CB5F6232242A8414 /* UserSessionTests.swift in Sources */, - 99F8DA4CCC6772EE5FE68E24 /* ViewModelContext.swift in Sources */, FB9A1DD83EF641A75ABBCE69 /* WaitlistScreenViewModelTests.swift in Sources */, 7F02063FB3D1C3E5601471A1 /* WelcomeScreenScreenViewModelTests.swift in Sources */, 3116693C5EB476E028990416 /* XCTestCase.swift in Sources */, diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift index 07ab521ce..3516ac60c 100644 --- a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift +++ b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift @@ -41,7 +41,7 @@ class EmojiPickerScreenViewModel: EmojiPickerScreenViewModelType, EmojiPickerScr switch viewAction { case let .search(searchString: searchString): Task { - let categories = await emojiProvider.getCategories(searchString: searchString) + let categories = await emojiProvider.categories(searchString: searchString) state.categories = convert(emojiCategories: categories) } case let .emojiTapped(emoji: emoji): @@ -56,7 +56,7 @@ class EmojiPickerScreenViewModel: EmojiPickerScreenViewModelType, EmojiPickerScr private func loadEmojis() { Task(priority: .userInitiated) { [weak self] in guard let self else { return } - let categories = await self.emojiProvider.getCategories(searchString: nil) + let categories = await self.emojiProvider.categories(searchString: nil) self.state.categories = convert(emojiCategories: categories) } } diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenModels.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenModels.swift index 7845255dc..c4f46eaf6 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenModels.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenModels.swift @@ -32,7 +32,7 @@ struct RoomMembersListScreenViewState: BindableState { init(joinedMembersCount: Int, joinedMembers: [RoomMemberDetails] = [], invitedMembers: [RoomMemberDetails] = [], - bindings: RoomMembersListScreenViewStateBindings = .init()) { + bindings: RoomMembersListScreenViewStateBindings) { self.joinedMembersCount = joinedMembersCount self.joinedMembers = joinedMembers self.invitedMembers = invitedMembers diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift index c04b70875..76ca09f40 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift @@ -37,7 +37,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe self.roomProxy = roomProxy self.userIndicatorController = userIndicatorController - super.init(initialViewState: .init(joinedMembersCount: roomProxy.joinedMembersCount), + super.init(initialViewState: .init(joinedMembersCount: roomProxy.joinedMembersCount, bindings: .init()), imageProvider: mediaProvider) setupMembers() @@ -83,7 +83,8 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe self.members = members self.state = .init(joinedMembersCount: roomProxy.joinedMembersCount, joinedMembers: roomMembersDetails.joinedMembers, - invitedMembers: roomMembersDetails.invitedMembers) + invitedMembers: roomMembersDetails.invitedMembers, + bindings: state.bindings) self.state.canInviteUsers = roomMembersDetails.accountOwner?.canInviteUsers ?? false hideLoader() } diff --git a/ElementX/Sources/Services/Emojis/EmojiProvider.swift b/ElementX/Sources/Services/Emojis/EmojiProvider.swift index ca95c49b4..83ab9ef7b 100644 --- a/ElementX/Sources/Services/Emojis/EmojiProvider.swift +++ b/ElementX/Sources/Services/Emojis/EmojiProvider.swift @@ -19,7 +19,7 @@ import Foundation @MainActor protocol EmojiProviderProtocol { - func getCategories(searchString: String?) async -> [EmojiCategory] + func categories(searchString: String?) async -> [EmojiCategory] } private enum EmojiProviderState { @@ -39,7 +39,7 @@ class EmojiProvider: EmojiProviderProtocol { } } - func getCategories(searchString: String? = nil) async -> [EmojiCategory] { + func categories(searchString: String? = nil) async -> [EmojiCategory] { let emojiCategories = await loadIfNeeded() if let searchString, searchString.isEmpty == false { return search(searchString: searchString, emojiCategories: emojiCategories) diff --git a/UnitTests/Sources/AnalyticsSettingsScreenViewModelTests.swift b/UnitTests/Sources/AnalyticsSettingsScreenViewModelTests.swift index cfcdaeb3d..64174b8a3 100644 --- a/UnitTests/Sources/AnalyticsSettingsScreenViewModelTests.swift +++ b/UnitTests/Sources/AnalyticsSettingsScreenViewModelTests.swift @@ -24,6 +24,10 @@ class AnalyticsSettingsScreenViewModelTests: XCTestCase { private var viewModel: AnalyticsSettingsScreenViewModelProtocol! private var context: AnalyticsSettingsScreenViewModelType.Context! + override func tearDown() { + appSettings.analyticsConsentState = .unknown + } + @MainActor override func setUpWithError() throws { AppSettings.reset() appSettings = AppSettings() diff --git a/UnitTests/Sources/BugReportViewModelTests.swift b/UnitTests/Sources/BugReportViewModelTests.swift index 2885fc996..683db20ec 100644 --- a/UnitTests/Sources/BugReportViewModelTests.swift +++ b/UnitTests/Sources/BugReportViewModelTests.swift @@ -71,18 +71,19 @@ class BugReportViewModelTests: XCTestCase { deviceID: nil, screenshot: nil, isModallyPresented: false) let context = viewModel.context - let deferred = deferFulfillment(viewModel.actions.collect(2).first()) + + let deferred = deferFulfillment(viewModel.actions) { action in + switch action { + case .submitFinished: + return true + default: + return false + } + } + context.send(viewAction: .submit) - let actions = try await deferred.fulfill() - - guard case .submitStarted = actions[0] else { - return XCTFail("Action 1 was not .submitFailed") - } - - guard case .submitFinished = actions[1] else { - return XCTFail("Action 2 was not .submitFinished") - } - + try await deferred.fulfill() + XCTAssert(mockService.submitBugReportProgressListenerCallsCount == 1) XCTAssert(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport == BugReport(userID: "@mock.client.com", deviceID: nil, text: "", includeLogs: true, includeCrashLog: true, canContact: false, githubLabels: [], files: [])) } @@ -97,18 +98,18 @@ class BugReportViewModelTests: XCTestCase { deviceID: nil, screenshot: nil, isModallyPresented: false) - let deferred = deferFulfillment(viewModel.actions.collect(2).first()) - let context = viewModel.context - context.send(viewAction: .submit) - let actions = try await deferred.fulfill() - - guard case .submitStarted = actions[0] else { - return XCTFail("Action 1 was not .submitFailed") + let deferred = deferFulfillment(viewModel.actions) { action in + switch action { + case .submitFailed: + return true + default: + return false + } } - guard case .submitFailed = actions[1] else { - return XCTFail("Action 2 was not .submitFailed") - } + let context = viewModel.context + context.send(viewAction: .submit) + try await deferred.fulfill() XCTAssert(mockService.submitBugReportProgressListenerCallsCount == 1) XCTAssert(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport == BugReport(userID: "@mock.client.com", deviceID: nil, text: "", includeLogs: true, includeCrashLog: true, canContact: false, githubLabels: [], files: [])) diff --git a/UnitTests/Sources/CreatePollScreenViewModelTests.swift b/UnitTests/Sources/CreatePollScreenViewModelTests.swift index 89251a7ed..599896002 100644 --- a/UnitTests/Sources/CreatePollScreenViewModelTests.swift +++ b/UnitTests/Sources/CreatePollScreenViewModelTests.swift @@ -44,8 +44,17 @@ class CreatePollScreenViewModelTests: XCTestCase { context.options[1].text = "bla2" XCTAssertFalse(context.viewState.bindings.isCreateButtonDisabled) - let deferred = deferFulfillment(viewModel.actions.first()) + let deferred = deferFulfillment(viewModel.actions) { action in + switch action { + case .create: + return true + default: + return false + } + } + context.send(viewAction: .create) + let action = try await deferred.fulfill() guard case .create(let question, let options, let kind) = action else { diff --git a/UnitTests/Sources/EmojiProviderTests.swift b/UnitTests/Sources/EmojiProviderTests.swift index fca074d0f..55b5d233e 100644 --- a/UnitTests/Sources/EmojiProviderTests.swift +++ b/UnitTests/Sources/EmojiProviderTests.swift @@ -18,46 +18,55 @@ import XCTest @testable import ElementX +@MainActor final class EmojiProviderTests: XCTestCase { - var sut: EmojiProvider! - private var emojiLoaderMock: EmojiLoaderMock! - - @MainActor override func setUp() { - emojiLoaderMock = EmojiLoaderMock() - sut = EmojiProvider(loader: emojiLoaderMock) - } - - func test_whenEmojisLoaded_categoriesAreLoadedFromLoader() async throws { + func testWhenEmojisLoadedCategoriesAreLoadedFromLoader() async throws { let item = EmojiItem(label: "test", unicode: "test", keywords: ["1", "2"], shortcodes: ["1", "2"], skins: ["🙂"]) let category = EmojiCategory(id: "test", emojis: [item]) + + let emojiLoaderMock = EmojiLoaderMock() emojiLoaderMock.categories = [category] - let categories = await sut.getCategories() + + let emojiProvider = EmojiProvider(loader: emojiLoaderMock) + + let categories = await emojiProvider.categories() XCTAssertEqual(emojiLoaderMock.categories, categories) } - - func test_whenEmojisLoadedAndSearchStringEmpty_allCategoriesReturned() async throws { + + func testWhenEmojisLoadedAndSearchStringEmptyAllCategoriesReturned() async throws { let item = EmojiItem(label: "test", unicode: "test", keywords: ["1", "2"], shortcodes: ["1", "2"], skins: ["🙂"]) let category = EmojiCategory(id: "test", emojis: [item]) + + let emojiLoaderMock = EmojiLoaderMock() emojiLoaderMock.categories = [category] - let categories = await sut.getCategories(searchString: "") + + let emojiProvider = EmojiProvider(loader: emojiLoaderMock) + + let categories = await emojiProvider.categories(searchString: "") XCTAssertEqual(emojiLoaderMock.categories, categories) } - - func test_whenEmojisLoadedSecondTime_cachedValuesAreUsed() async throws { + + func testWhenEmojisLoadedSecondTimeCachedValuesAreUsed() async throws { let item = EmojiItem(label: "test", unicode: "test", keywords: ["1", "2"], shortcodes: ["1", "2"], skins: ["🙂"]) let item2 = EmojiItem(label: "test2", unicode: "test2", keywords: ["3", "4"], shortcodes: ["3", "4"], skins: ["🙂"]) let categoriesForFirstLoad = [EmojiCategory(id: "test", emojis: [item])] let categoriesForSecondLoad = [EmojiCategory(id: "test2", emojis: [item2])] + + let emojiLoaderMock = EmojiLoaderMock() emojiLoaderMock.categories = categoriesForFirstLoad - _ = await sut.getCategories() + + let emojiProvider = EmojiProvider(loader: emojiLoaderMock) + + _ = await emojiProvider.categories() emojiLoaderMock.categories = categoriesForSecondLoad - let categories = await sut.getCategories() + + let categories = await emojiProvider.categories() XCTAssertEqual(categories, categoriesForFirstLoad) } - func test_whenEmojisSearched_correctNumberOfCategoriesReturned() async throws { + func testWhenEmojisSearchedCorrectNumberOfCategoriesReturned() async throws { let searchString = "smile" var categories = [EmojiCategory]() let item0WithSearchString = EmojiItem(label: "emoji0", unicode: "\(searchString)_123", keywords: ["key1", "key1"], shortcodes: ["key1", "key1"], skins: ["🙂"]) @@ -74,9 +83,14 @@ final class EmojiProviderTests: XCTestCase { item4WithoutSearchString])) categories.append(EmojiCategory(id: "test", emojis: [item5WithSearchString])) + + let emojiLoaderMock = EmojiLoaderMock() emojiLoaderMock.categories = categories - _ = await sut.getCategories() - let result = await sut.getCategories(searchString: searchString) + + let emojiProvider = EmojiProvider(loader: emojiLoaderMock) + + _ = await emojiProvider.categories() + let result = await emojiProvider.categories(searchString: searchString) XCTAssertEqual(result.count, 2) XCTAssertEqual(result.first?.emojis.count, 4) } diff --git a/UnitTests/Sources/Extensions/ViewModelContext.swift b/UnitTests/Sources/Extensions/ViewModelContext.swift deleted file mode 100644 index 431f0bc4c..000000000 --- a/UnitTests/Sources/Extensions/ViewModelContext.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// 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. -// - -@testable import ElementX - -extension StateStoreViewModel.Context { - @discardableResult - func nextViewState() async -> State? { - await $viewState.nextValue - } -} diff --git a/UnitTests/Sources/Extensions/XCTestCase.swift b/UnitTests/Sources/Extensions/XCTestCase.swift index 58604eced..6a4b98887 100644 --- a/UnitTests/Sources/Extensions/XCTestCase.swift +++ b/UnitTests/Sources/Extensions/XCTestCase.swift @@ -19,34 +19,37 @@ 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. + /// - message: An optional custom expectation message + /// - until: callback that evaluates outputs until some condition is reached /// - Returns: The deferred fulfilment to be executed after some actions and that returns the result of the publisher. - func deferFulfillment(_ publisher: T, timeout: TimeInterval = 10, message: String? = nil) -> DeferredFulfillment { - var result: Result? + func deferFulfillment(_ publisher: P, + timeout: TimeInterval = 10, + message: String? = nil, + until condition: @escaping (P.Output) -> Bool) -> DeferredFulfillment { + var result: Result? let expectation = expectation(description: message ?? "Awaiting publisher") + var hasFullfilled = false let cancellable = publisher .sink { completion in switch completion { case .failure(let error): result = .failure(error) + expectation.fulfill() case .finished: break } - expectation.fulfill() } receiveValue: { value in - result = .success(value) + if condition(value), !hasFullfilled { + result = .success(value) + expectation.fulfill() + hasFullfilled = true + } } - return DeferredFulfillment { + return DeferredFulfillment { await self.fulfillment(of: [expectation], timeout: timeout) cancellable.cancel() let unwrappedResult = try XCTUnwrap(result, "Awaited publisher did not produce any output") @@ -54,6 +57,32 @@ extension XCTestCase { } } + /// XCTest 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. + /// - message: An optional custom expectation message + /// - Returns: The deferred fulfilment to be executed after some actions and that returns the result of the publisher. + func deferFulfillment, V: Equatable>(_ publisher: P, + keyPath: K, + transitionValues: [V], + timeout: TimeInterval = 10, + message: String? = nil) -> DeferredFulfillment { + var expectedOrder = transitionValues + let deferred = deferFulfillment

(publisher, timeout: timeout, message: message) { value in + let receivedValue = value[keyPath: keyPath] + if let index = expectedOrder.firstIndex(where: { $0 == receivedValue }), index == 0 { + expectedOrder.remove(at: index) + } + + return expectedOrder.isEmpty + } + + return deferred + } + struct DeferredFulfillment { let closure: () async throws -> T @discardableResult func fulfill() async throws -> T { diff --git a/UnitTests/Sources/HomeScreenViewModelTests.swift b/UnitTests/Sources/HomeScreenViewModelTests.swift index e3c80aeb0..aa069cbe4 100644 --- a/UnitTests/Sources/HomeScreenViewModelTests.swift +++ b/UnitTests/Sources/HomeScreenViewModelTests.swift @@ -83,8 +83,15 @@ class HomeScreenViewModelTests: XCTestCase { func testLeaveRoomAlert() async throws { let mockRoomId = "1" clientProxy.roomForIdentifierMocks[mockRoomId] = .init(with: .init(id: mockRoomId, displayName: "Some room")) + + let deferred = deferFulfillment(context.$viewState) { value in + value.bindings.leaveRoomAlertItem != nil + } + context.send(viewAction: .leaveRoom(roomIdentifier: mockRoomId)) - await context.nextViewState() + + try await deferred.fulfill() + XCTAssertEqual(context.leaveRoomAlertItem?.roomId, mockRoomId) } @@ -93,9 +100,16 @@ 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) { value in + value.bindings.alertInfo != nil + } + context.send(viewAction: .confirmLeaveRoom(roomIdentifier: mockRoomId)) - let state = await context.nextViewState() - XCTAssertNotNil(state?.bindings.alertInfo) + + try await deferred.fulfill() + + XCTAssertNotNil(context.alertInfo) } func testLeaveRoomSuccess() async throws { diff --git a/UnitTests/Sources/InviteUsersViewModelTests.swift b/UnitTests/Sources/InviteUsersViewModelTests.swift index 87253bdf3..0b4a7efaf 100644 --- a/UnitTests/Sources/InviteUsersViewModelTests.swift +++ b/UnitTests/Sources/InviteUsersViewModelTests.swift @@ -67,25 +67,26 @@ class InviteUsersScreenViewModelTests: XCTestCase { let mockedMembers: [RoomMemberProxyMock] = [.mockAlice, .mockBob] setupWithRoomType(roomType: .room(roomProxy: RoomProxyMock(with: .init(displayName: "test", members: mockedMembers)))) - let deferredState = deferFulfillment(viewModel.context.$viewState - .map(\.membershipState) - .map(\.isEmpty) - .removeDuplicates() - .collect(2).first(), message: "2 states should be published.") - context.send(viewAction: .toggleUser(.mockAlice)) - - let states = try await deferredState.fulfill() - XCTAssertEqual(states, [true, false]) - - let deferredAction = deferFulfillment(viewModel.actions.first(), message: "1 action should be published.") - - Task.detached(priority: .low) { - await self.context.send(viewAction: .proceed) + let deferredState = deferFulfillment(viewModel.context.$viewState) { state in + state.isUserSelected(.mockAlice) } - let action = try await deferredAction.fulfill() + context.send(viewAction: .toggleUser(.mockAlice)) - guard case let .invite(members) = action else { + try await deferredState.fulfill() + + let deferredAction = deferFulfillment(viewModel.actions) { action in + switch action { + case .invite: + return true + default: + return false + } + } + + context.send(viewAction: .proceed) + + guard case let .invite(members) = try await deferredAction.fulfill() else { XCTFail("Sent action should be 'invite'") return } diff --git a/UnitTests/Sources/InvitesScreenViewModelTests.swift b/UnitTests/Sources/InvitesScreenViewModelTests.swift index 0822e7d62..f47babf6e 100644 --- a/UnitTests/Sources/InvitesScreenViewModelTests.swift +++ b/UnitTests/Sources/InvitesScreenViewModelTests.swift @@ -56,7 +56,13 @@ class InvitesScreenViewModelTests: XCTestCase { } setupViewModel(roomSummaries: invites) - let deferred = deferFulfillment(viewModel.actions.first(), message: "1 action should be published.") + let deferred = deferFulfillment(viewModel.actions) { action in + switch action { + case .openRoom: + return true + } + } + context.send(viewAction: .accept(.init(roomDetails: details, isUnread: false))) let action = try await deferred.fulfill() diff --git a/UnitTests/Sources/NotificationSettingsEditScreenViewModelTests.swift b/UnitTests/Sources/NotificationSettingsEditScreenViewModelTests.swift index a7eec6800..88ad3ef38 100644 --- a/UnitTests/Sources/NotificationSettingsEditScreenViewModelTests.swift +++ b/UnitTests/Sources/NotificationSettingsEditScreenViewModelTests.swift @@ -49,9 +49,12 @@ class NotificationSettingsEditScreenViewModelTests: XCTestCase { userSession: userSession, notificationSettingsProxy: notificationSettingsProxy) - let deferred = deferFulfillment(viewModel.context.$viewState.map(\.defaultMode) - .first(where: { !$0.isNil })) + let deferred = deferFulfillment(viewModel.context.$viewState) { state in + state.defaultMode != nil + } + viewModel.fetchInitialContent() + try await deferred.fulfill() // `getDefaultRoomNotificationModeIsEncryptedIsOneToOne` must have been called twice (for encrypted and unencrypted group chats) @@ -74,20 +77,19 @@ class NotificationSettingsEditScreenViewModelTests: XCTestCase { viewModel = NotificationSettingsEditScreenViewModel(chatType: .groupChat, userSession: userSession, notificationSettingsProxy: notificationSettingsProxy) - let deferred = deferFulfillment(viewModel.context.$viewState.map(\.defaultMode) - .first(where: { !$0.isNil })) + let deferred = deferFulfillment(viewModel.context.$viewState) { state in + state.defaultMode != nil + } + viewModel.fetchInitialContent() + try await deferred.fulfill() - // Set mode to .allMessages - let deferredViewState = deferFulfillment(context.$viewState - .map(\.pendingMode) - .removeDuplicates() - .collect(3).first()) + var deferredViewState = deferFulfillment(viewModel.context.$viewState, keyPath: \.pendingMode, transitionValues: [nil, .allMessages, nil]) + context.send(viewAction: .setMode(.allMessages)) - let pendingModes = try await deferredViewState.fulfill() - XCTAssertEqual(pendingModes, [nil, .allMessages, nil]) + try await deferredViewState.fulfill() // `setDefaultRoomNotificationModeIsEncryptedIsOneToOneMode` must have been called twice (for encrypted and unencrypted group chats) let invocations = notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeReceivedInvocations @@ -101,34 +103,36 @@ class NotificationSettingsEditScreenViewModelTests: XCTestCase { XCTAssertEqual(invocations[1].isOneToOne, false) XCTAssertEqual(invocations[1].mode, .allMessages) - // The default mode should be updated - let deferredNewViewState = deferFulfillment(context.$viewState - .map(\.defaultMode) - .first(where: { $0 == .allMessages })) - try await deferredNewViewState.fulfill() - + deferredViewState = deferFulfillment(viewModel.context.$viewState, + keyPath: \.defaultMode, + transitionValues: [.allMessages]) + + try await deferredViewState.fulfill() + XCTAssertEqual(context.viewState.defaultMode, .allMessages) XCTAssertNil(context.viewState.bindings.alertInfo) } - + func testSetModeMentions() async throws { viewModel = NotificationSettingsEditScreenViewModel(chatType: .groupChat, userSession: userSession, notificationSettingsProxy: notificationSettingsProxy) - let deferred = deferFulfillment(viewModel.context.$viewState.map(\.defaultMode) - .first(where: { !$0.isNil })) - viewModel.fetchInitialContent() - try await deferred.fulfill() - - // Set mode to .allMessages - let deferredViewState = deferFulfillment(context.$viewState - .map(\.pendingMode) - .removeDuplicates() - .collect(3).first()) - context.send(viewAction: .setMode(.mentionsAndKeywordsOnly)) - let pendingModes = try await deferredViewState.fulfill() - XCTAssertEqual(pendingModes, [nil, .mentionsAndKeywordsOnly, nil]) + let deferred = deferFulfillment(viewModel.context.$viewState) { state in + state.defaultMode != nil + } + + viewModel.fetchInitialContent() + + try await deferred.fulfill() + + var deferredViewState = deferFulfillment(viewModel.context.$viewState, + keyPath: \.pendingMode, + transitionValues: [nil, .mentionsAndKeywordsOnly, nil]) + + context.send(viewAction: .setMode(.mentionsAndKeywordsOnly)) + + try await deferredViewState.fulfill() // `setDefaultRoomNotificationModeIsEncryptedIsOneToOneMode` must have been called twice (for encrypted and unencrypted group chats) let invocations = notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeReceivedInvocations @@ -142,37 +146,39 @@ class NotificationSettingsEditScreenViewModelTests: XCTestCase { XCTAssertEqual(invocations[1].isOneToOne, false) XCTAssertEqual(invocations[1].mode, .mentionsAndKeywordsOnly) - // The default mode should be updated - let deferredNewViewState = deferFulfillment(context.$viewState - .map(\.defaultMode) - .first(where: { $0 == .mentionsAndKeywordsOnly })) - try await deferredNewViewState.fulfill() - + deferredViewState = deferFulfillment(viewModel.context.$viewState, + keyPath: \.defaultMode, + transitionValues: [.mentionsAndKeywordsOnly]) + + try await deferredViewState.fulfill() + XCTAssertEqual(context.viewState.defaultMode, .mentionsAndKeywordsOnly) XCTAssertNil(context.viewState.bindings.alertInfo) } - + func testSetModeDirectChats() async throws { notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .mentionsAndKeywordsOnly // Initialize for direct chats viewModel = NotificationSettingsEditScreenViewModel(chatType: .oneToOneChat, userSession: userSession, notificationSettingsProxy: notificationSettingsProxy) - let deferred = deferFulfillment(viewModel.context.$viewState.map(\.defaultMode) - .first(where: { !$0.isNil })) + + let deferred = deferFulfillment(viewModel.context.$viewState) { state in + state.defaultMode != nil + } + viewModel.fetchInitialContent() + try await deferred.fulfill() - - // Set mode to .allMessages - let deferredViewState = deferFulfillment(context.$viewState - .map(\.pendingMode) - .removeDuplicates() - .collect(3).first()) + + let deferredViewState = deferFulfillment(viewModel.context.$viewState, + keyPath: \.pendingMode, + transitionValues: [nil, .allMessages, nil]) + context.send(viewAction: .setMode(.allMessages)) - let pendingModes = try await deferredViewState.fulfill() - - XCTAssertEqual(pendingModes, [nil, .allMessages, nil]) + try await deferredViewState.fulfill() + // `setDefaultRoomNotificationModeIsEncryptedIsOneToOneMode` must have been called twice (for encrypted and unencrypted direct chats) let invocations = notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeReceivedInvocations XCTAssertEqual(notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount, 2) @@ -185,43 +191,53 @@ class NotificationSettingsEditScreenViewModelTests: XCTestCase { XCTAssertEqual(invocations[1].isOneToOne, true) XCTAssertEqual(invocations[1].mode, .allMessages) } - + func testSetModeFailure() async throws { notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .mentionsAndKeywordsOnly notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeThrowableError = NotificationSettingsError.Generic(message: "error") viewModel = NotificationSettingsEditScreenViewModel(chatType: .oneToOneChat, userSession: userSession, notificationSettingsProxy: notificationSettingsProxy) - let deferred = deferFulfillment(viewModel.context.$viewState.map(\.defaultMode) - .first(where: { !$0.isNil })) - viewModel.fetchInitialContent() - try await deferred.fulfill() - - // Set mode to .allMessages - let deferredViewState = deferFulfillment(context.$viewState - .map(\.pendingMode) - .removeDuplicates() - .collect(3).first()) - context.send(viewAction: .setMode(.allMessages)) - let pendingModes = try await deferredViewState.fulfill() - XCTAssertEqual(pendingModes, [nil, .allMessages, nil]) + let deferred = deferFulfillment(viewModel.context.$viewState) { state in + state.defaultMode != nil + } + + viewModel.fetchInitialContent() + + try await deferred.fulfill() + + let deferredViewState = deferFulfillment(viewModel.context.$viewState, + keyPath: \.pendingMode, + transitionValues: [nil, .allMessages, nil]) + + context.send(viewAction: .setMode(.allMessages)) + + try await deferredViewState.fulfill() + XCTAssertNotNil(context.viewState.bindings.alertInfo) } - + func testSelectRoom() async throws { let roomID = "!roomidentifier:matrix.org" viewModel = NotificationSettingsEditScreenViewModel(chatType: .oneToOneChat, userSession: userSession, notificationSettingsProxy: notificationSettingsProxy) - let deferredActions = deferFulfillment(viewModel.actions.first()) + let deferredActions = deferFulfillment(viewModel.actions) { action in + switch action { + case .requestRoomNotificationSettingsPresentation: + return true + } + } + context.send(viewAction: .selectRoom(roomIdentifier: roomID)) - let sentActions = try await deferredActions.fulfill() + + let sentAction = try await deferredActions.fulfill() let expectedAction = NotificationSettingsEditScreenViewModelAction.requestRoomNotificationSettingsPresentation(roomID: roomID) - guard case let .requestRoomNotificationSettingsPresentation(roomID: receivedRoomID) = sentActions, receivedRoomID == roomID else { - XCTFail("Expected action \(expectedAction), but was \(sentActions)") + guard case let .requestRoomNotificationSettingsPresentation(roomID: receivedRoomID) = sentAction, receivedRoomID == roomID else { + XCTFail("Expected action \(expectedAction), but was \(sentAction)") return } } diff --git a/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift b/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift index 3eef3e6f5..9d1af3064 100644 --- a/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift +++ b/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift @@ -27,10 +27,10 @@ class NotificationSettingsScreenViewModelTests: XCTestCase { private var userSession: UserSessionProtocol! private var userNotificationCenter: UserNotificationCenterMock! private var notificationSettingsProxy: NotificationSettingsProxyMock! - + @MainActor override func setUpWithError() throws { AppSettings.reset() - + userNotificationCenter = UserNotificationCenterMock() userNotificationCenter.authorizationStatusReturnValue = .authorized appSettings = AppSettings() @@ -38,10 +38,10 @@ class NotificationSettingsScreenViewModelTests: XCTestCase { notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .allMessages notificationSettingsProxy.isRoomMentionEnabledReturnValue = true notificationSettingsProxy.isCallEnabledReturnValue = true - + let clientProxy = MockClientProxy(userID: "@a:b.com") userSession = MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider()) - + viewModel = NotificationSettingsScreenViewModel(userSession: userSession, appSettings: appSettings, userNotificationCenter: userNotificationCenter, @@ -49,19 +49,19 @@ class NotificationSettingsScreenViewModelTests: XCTestCase { isModallyPresented: false) context = viewModel.context } - + func testEnableNotifications() { appSettings.enableNotifications = false context.send(viewAction: .changedEnableNotifications) XCTAssertTrue(appSettings.enableNotifications) } - + func testDisableNotifications() { appSettings.enableNotifications = true context.send(viewAction: .changedEnableNotifications) XCTAssertFalse(appSettings.enableNotifications) } - + func testFetchSettings() async throws { notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in switch (isEncrypted, isOneToOne) { @@ -71,21 +71,25 @@ class NotificationSettingsScreenViewModelTests: XCTestCase { return .mentionsAndKeywordsOnly } } - let deferred = deferFulfillment(viewModel.context.$viewState.map(\.settings) - .first(where: { $0 != nil })) - notificationSettingsProxy.callbacks.send(.settingsDidChange) - try await deferred.fulfill() + let deferred = deferFulfillment(viewModel.context.$viewState) { state in + state.settings != nil + } + + notificationSettingsProxy.callbacks.send(.settingsDidChange) + + try await deferred.fulfill() + XCTAssertEqual(notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneCallsCount, 4) XCTAssert(notificationSettingsProxy.isRoomMentionEnabledCalled) XCTAssert(notificationSettingsProxy.isCallEnabledCalled) - + XCTAssertEqual(context.viewState.settings?.groupChatsMode, .mentionsAndKeywordsOnly) XCTAssertEqual(context.viewState.settings?.directChatsMode, .allMessages) XCTAssertEqual(context.viewState.settings?.inconsistentSettings, []) XCTAssertNil(context.viewState.bindings.alertInfo) } - + func testInconsistentGroupChatsSettings() async throws { notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in switch (isEncrypted, isOneToOne) { @@ -97,16 +101,19 @@ class NotificationSettingsScreenViewModelTests: XCTestCase { return .allMessages } } - - let deferred = deferFulfillment(viewModel.context.$viewState.map(\.settings) - .first(where: { $0 != nil })) - notificationSettingsProxy.callbacks.send(.settingsDidChange) - try await deferred.fulfill() + + let deferred = deferFulfillment(viewModel.context.$viewState) { state in + state.settings != nil + } + notificationSettingsProxy.callbacks.send(.settingsDidChange) + + try await deferred.fulfill() + XCTAssertEqual(context.viewState.settings?.groupChatsMode, .allMessages) XCTAssertEqual(context.viewState.settings?.inconsistentSettings, [.init(chatType: .groupChat, isEncrypted: false)]) } - + func testInconsistentDirectChatsSettings() async throws { notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in switch (isEncrypted, isOneToOne) { @@ -118,16 +125,19 @@ class NotificationSettingsScreenViewModelTests: XCTestCase { return .allMessages } } - - let deferred = deferFulfillment(viewModel.context.$viewState.map(\.settings) - .first(where: { $0 != nil })) + + let deferred = deferFulfillment(viewModel.context.$viewState) { state in + state.settings != nil + } + notificationSettingsProxy.callbacks.send(.settingsDidChange) + try await deferred.fulfill() - + XCTAssertEqual(context.viewState.settings?.directChatsMode, .allMessages) XCTAssertEqual(context.viewState.settings?.inconsistentSettings, [.init(chatType: .oneToOneChat, isEncrypted: false)]) } - + func testFixInconsistentSettings() async throws { // Initialize with a configuration mismatch where encrypted one-to-one chats is `.allMessages` and unencrypted one-to-one chats is `.mentionsAndKeywordsOnly` notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in @@ -140,25 +150,24 @@ class NotificationSettingsScreenViewModelTests: XCTestCase { return .allMessages } } - - let deferred = deferFulfillment(viewModel.context.$viewState.map(\.settings) - .first(where: { $0 != nil })) - notificationSettingsProxy.callbacks.send(.settingsDidChange) - try await deferred.fulfill() + + var deferred = deferFulfillment(viewModel.context.$viewState) { state in + state.settings != nil + } + notificationSettingsProxy.callbacks.send(.settingsDidChange) + + try await deferred.fulfill() + XCTAssertEqual(context.viewState.settings?.directChatsMode, .allMessages) XCTAssertEqual(context.viewState.settings?.inconsistentSettings, [.init(chatType: .oneToOneChat, isEncrypted: false)]) - let deferredState = deferFulfillment(viewModel.context.$viewState - .map(\.fixingConfigurationMismatch) - .removeDuplicates() - .collect(3) - .first()) + deferred = deferFulfillment(viewModel.context.$viewState, keyPath: \.fixingConfigurationMismatch, transitionValues: [false, true, false]) + context.send(viewAction: .fixConfigurationMismatchTapped) - let fixingStates = try await deferredState.fulfill() - - XCTAssertEqual(fixingStates, [false, true, false]) + try await deferred.fulfill() + // Ensure we only fix the invalid setting: unencrypted one-to-one chats should be set to `.allMessages` (to match encrypted one-to-one chats) XCTAssertEqual(notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount, 1) let callArguments = notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeReceivedArguments @@ -166,7 +175,7 @@ class NotificationSettingsScreenViewModelTests: XCTestCase { XCTAssertEqual(callArguments?.isOneToOne, true) XCTAssertEqual(callArguments?.mode, .allMessages) } - + func testFixAllInconsistentSettings() async throws { // Initialize with a configuration mismatch where // - encrypted one-to-one chats is `.allMessages` and unencrypted one-to-one chats is `.mentionsAndKeywordsOnly` @@ -179,25 +188,32 @@ class NotificationSettingsScreenViewModelTests: XCTestCase { return .mentionsAndKeywordsOnly } } - - let deferred = deferFulfillment(viewModel.context.$viewState.map(\.settings) - .first(where: { $0 != nil })) + + var deferred = deferFulfillment(viewModel.context.$viewState) { state in + state.settings != nil + } + notificationSettingsProxy.callbacks.send(.settingsDidChange) + try await deferred.fulfill() - + XCTAssertEqual(context.viewState.settings?.directChatsMode, .allMessages) XCTAssertEqual(context.viewState.settings?.inconsistentSettings, [.init(chatType: .groupChat, isEncrypted: false), .init(chatType: .oneToOneChat, isEncrypted: false)]) + + deferred = deferFulfillment(viewModel.context.$viewState) { state in + state.fixingConfigurationMismatch == true + } - let deferredState = deferFulfillment(viewModel.context.$viewState - .map(\.fixingConfigurationMismatch) - .removeDuplicates() - .collect(3) - .first()) context.send(viewAction: .fixConfigurationMismatchTapped) - let fixingStates = try await deferredState.fulfill() - XCTAssertEqual(fixingStates, [false, true, false]) + try await deferred.fulfill() + deferred = deferFulfillment(viewModel.context.$viewState) { state in + state.fixingConfigurationMismatch == false + } + + try await deferred.fulfill() + // All problems should be fixed XCTAssertEqual(notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount, 2) let callArguments = notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeReceivedInvocations @@ -210,112 +226,164 @@ class NotificationSettingsScreenViewModelTests: XCTestCase { XCTAssertEqual(callArguments[1].isOneToOne, true) XCTAssertEqual(callArguments[1].mode, .allMessages) } - + func testToggleRoomMentionOff() async throws { notificationSettingsProxy.isRoomMentionEnabledReturnValue = true - let deferredInitialFetch = deferFulfillment(viewModel.context.$viewState.map(\.settings) - .first(where: { $0 != nil })) + + let deferredState = deferFulfillment(viewModel.context.$viewState) { state in + state.settings != nil + } + notificationSettingsProxy.callbacks.send(.settingsDidChange) - try await deferredInitialFetch.fulfill() + try await deferredState.fulfill() + context.roomMentionsEnabled = false - let deferred = deferFulfillment(notificationSettingsProxy.callbacks - .first(where: { $0 == .settingsDidChange })) - context.send(viewAction: .roomMentionChanged) - try await deferred.fulfill() + let deferred = deferFulfillment(notificationSettingsProxy.callbacks) { callback in + callback == .settingsDidChange + } + + context.send(viewAction: .roomMentionChanged) + + try await deferred.fulfill() + XCTAssert(notificationSettingsProxy.setRoomMentionEnabledEnabledCalled) XCTAssertEqual(notificationSettingsProxy.setRoomMentionEnabledEnabledReceivedEnabled, false) } - + func testToggleRoomMentionOn() async throws { notificationSettingsProxy.isRoomMentionEnabledReturnValue = false - let deferredInitialFetch = deferFulfillment(viewModel.context.$viewState.map(\.settings) - .first(where: { $0 != nil })) + + let deferredInitialFetch = deferFulfillment(viewModel.context.$viewState) { state in + state.settings != nil + } + viewModel.fetchInitialContent() try await deferredInitialFetch.fulfill() context.roomMentionsEnabled = true - let deferred = deferFulfillment(notificationSettingsProxy.callbacks - .first(where: { $0 == .settingsDidChange })) - context.send(viewAction: .roomMentionChanged) - try await deferred.fulfill() + let deferred = deferFulfillment(notificationSettingsProxy.callbacks) { callback in + callback == .settingsDidChange + } + + context.send(viewAction: .roomMentionChanged) + + try await deferred.fulfill() + XCTAssert(notificationSettingsProxy.setRoomMentionEnabledEnabledCalled) XCTAssertEqual(notificationSettingsProxy.setRoomMentionEnabledEnabledReceivedEnabled, true) } - + func testToggleRoomMentionFailure() async throws { notificationSettingsProxy.setRoomMentionEnabledEnabledThrowableError = NotificationSettingsError.Generic(message: "error") notificationSettingsProxy.isRoomMentionEnabledReturnValue = false - let deferredInitialFetch = deferFulfillment(viewModel.context.$viewState.map(\.settings) - .first(where: { $0 != nil })) + + let deferredInitialFetch = deferFulfillment(viewModel.context.$viewState) { state in + state.settings != nil + } + viewModel.fetchInitialContent() + try await deferredInitialFetch.fulfill() - + context.roomMentionsEnabled = true - let deferred = deferFulfillment(context.$viewState.map(\.applyingChange) - .removeDuplicates() - .collect(3) - .first()) + + var deferred = deferFulfillment(context.$viewState) { state in + state.applyingChange == true + } + context.send(viewAction: .roomMentionChanged) - let states = try await deferred.fulfill() - XCTAssertEqual(states, [false, true, false]) - XCTAssertNotNil(context.alertInfo) - } - - func testToggleCallsOff() async throws { - notificationSettingsProxy.isCallEnabledReturnValue = true - let deferredInitialFetch = deferFulfillment(viewModel.context.$viewState.map(\.settings) - .first(where: { $0 != nil })) - viewModel.fetchInitialContent() - try await deferredInitialFetch.fulfill() - - context.callsEnabled = false - let deferred = deferFulfillment(notificationSettingsProxy.callbacks - .first(where: { $0 == .settingsDidChange })) - context.send(viewAction: .callsChanged) try await deferred.fulfill() + deferred = deferFulfillment(context.$viewState) { state in + state.applyingChange == false + } + + try await deferred.fulfill() + + XCTAssertNotNil(context.alertInfo) + } + + func testToggleCallsOff() async throws { + notificationSettingsProxy.isCallEnabledReturnValue = true + + let deferredInitialFetch = deferFulfillment(viewModel.context.$viewState) { state in + state.settings != nil + } + + viewModel.fetchInitialContent() + + try await deferredInitialFetch.fulfill() + + context.callsEnabled = false + let deferred = deferFulfillment(notificationSettingsProxy.callbacks) { callback in + callback == .settingsDidChange + } + + context.send(viewAction: .callsChanged) + + try await deferred.fulfill() + XCTAssert(notificationSettingsProxy.setCallEnabledEnabledCalled) XCTAssertEqual(notificationSettingsProxy.setCallEnabledEnabledReceivedEnabled, false) } - + func testToggleCallsOn() async throws { notificationSettingsProxy.isCallEnabledReturnValue = false - let deferredInitialFetch = deferFulfillment(viewModel.context.$viewState.map(\.settings) - .first(where: { $0 != nil })) + + let deferredInitialFetch = deferFulfillment(viewModel.context.$viewState) { state in + state.settings != nil + } + viewModel.fetchInitialContent() + try await deferredInitialFetch.fulfill() context.callsEnabled = true - let deferred = deferFulfillment(notificationSettingsProxy.callbacks - .first(where: { $0 == .settingsDidChange })) + + let deferred = deferFulfillment(notificationSettingsProxy.callbacks) { callback in + callback == .settingsDidChange + } + context.send(viewAction: .callsChanged) + try await deferred.fulfill() XCTAssert(notificationSettingsProxy.setCallEnabledEnabledCalled) XCTAssertEqual(notificationSettingsProxy.setCallEnabledEnabledReceivedEnabled, true) } - + func testToggleCallsFailure() async throws { notificationSettingsProxy.setCallEnabledEnabledThrowableError = NotificationSettingsError.Generic(message: "error") notificationSettingsProxy.isCallEnabledReturnValue = false - let deferredInitialFetch = deferFulfillment(viewModel.context.$viewState.map(\.settings) - .first(where: { $0 != nil })) + + let deferredInitialFetch = deferFulfillment(viewModel.context.$viewState) { state in + state.settings != nil + } + viewModel.fetchInitialContent() + try await deferredInitialFetch.fulfill() context.callsEnabled = true - let deferred = deferFulfillment(context.$viewState.map(\.applyingChange) - .removeDuplicates() - .collect(3) - .first()) - context.send(viewAction: .callsChanged) - let states = try await deferred.fulfill() - XCTAssertEqual(states, [false, true, false]) + var deferred = deferFulfillment(context.$viewState) { state in + state.applyingChange == true + } + + context.send(viewAction: .callsChanged) + + try await deferred.fulfill() + + deferred = deferFulfillment(context.$viewState) { state in + state.applyingChange == false + } + + try await deferred.fulfill() + XCTAssertNotNil(context.alertInfo) } } diff --git a/UnitTests/Sources/ReportContentViewModelTests.swift b/UnitTests/Sources/ReportContentViewModelTests.swift index 560235b6b..c6f61278e 100644 --- a/UnitTests/Sources/ReportContentViewModelTests.swift +++ b/UnitTests/Sources/ReportContentViewModelTests.swift @@ -31,15 +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) - let actions = try await deferred.fulfill() - XCTAssertEqual(actions, [.submitStarted, .submitFinished]) + let deferred = deferFulfillment(viewModel.actions) { action in + action == .submitFinished + } + + try await deferred.fulfill() // Then the content should be reported, but the user should not be included. XCTAssertEqual(roomProxy.reportContentReasonCallsCount, 1, "The content should always be reported.") @@ -62,10 +63,13 @@ class ReportContentScreenViewModelTests: XCTestCase { viewModel.state.bindings.reasonText = reportReason viewModel.state.bindings.ignoreUser = true - let deferred = deferFulfillment(viewModel.actions.collect(2).first()) viewModel.context.send(viewAction: .submit) - let result = try await deferred.fulfill() - XCTAssertEqual(result, [.submitStarted, .submitFinished]) + + let deferred = deferFulfillment(viewModel.actions) { action in + action == .submitFinished + } + + try await deferred.fulfill() // Then the content should be reported, and the user should be ignored. XCTAssertEqual(roomProxy.reportContentReasonCallsCount, 1, "The content should always be reported.") diff --git a/UnitTests/Sources/RoomDetailsEditScreenViewModelTests.swift b/UnitTests/Sources/RoomDetailsEditScreenViewModelTests.swift index 644f2dbff..feab25d22 100644 --- a/UnitTests/Sources/RoomDetailsEditScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomDetailsEditScreenViewModelTests.swift @@ -95,9 +95,13 @@ class RoomDetailsEditScreenViewModelTests: XCTestCase { setupViewModel(accountOwner: .mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]), roomProxyConfiguration: .init(name: "Some room", displayName: "Some room")) - let deferred = deferFulfillment(viewModel.actions.first()) + let deferred = deferFulfillment(viewModel.actions) { action in + action == .saveFinished + } + context.name = "name" context.send(viewAction: .save) + let action = try await deferred.fulfill() XCTAssertEqual(action, .saveFinished) } diff --git a/UnitTests/Sources/RoomDetailsViewModelTests.swift b/UnitTests/Sources/RoomDetailsViewModelTests.swift index bd0bc1093..8acce284d 100644 --- a/UnitTests/Sources/RoomDetailsViewModelTests.swift +++ b/UnitTests/Sources/RoomDetailsViewModelTests.swift @@ -50,13 +50,15 @@ class RoomDetailsScreenViewModelTests: XCTestCase { mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration())) - let deferred = deferFulfillment(context.$viewState.collect(2).first()) + let deferred = deferFulfillment(context.$viewState) { state in + state.bindings.leaveRoomAlertItem != nil + } + context.send(viewAction: .processTapLeave) - let states = try await deferred.fulfill() - - XCTAssertNil(states[0].bindings.leaveRoomAlertItem) - XCTAssertEqual(states[1].bindings.leaveRoomAlertItem?.state, .public) - XCTAssertEqual(states[1].bindings.leaveRoomAlertItem?.subtitle, L10n.leaveRoomAlertSubtitle) + try await deferred.fulfill() + + XCTAssertEqual(context.viewState.bindings.leaveRoomAlertItem?.state, .public) + XCTAssertEqual(context.viewState.bindings.leaveRoomAlertItem?.subtitle, L10n.leaveRoomAlertSubtitle) } func testLeaveRoomTappedWhenRoomNotPublic() async throws { @@ -67,13 +69,16 @@ class RoomDetailsScreenViewModelTests: XCTestCase { mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration())) - let deferred = deferFulfillment(context.$viewState.collect(2).first()) + let deferred = deferFulfillment(context.$viewState) { state in + state.bindings.leaveRoomAlertItem != nil + } + context.send(viewAction: .processTapLeave) - let states = try await deferred.fulfill() - context.send(viewAction: .processTapLeave) - XCTAssertNil(states[0].bindings.leaveRoomAlertItem) - XCTAssertEqual(states[1].bindings.leaveRoomAlertItem?.state, .private) - XCTAssertEqual(states[1].bindings.leaveRoomAlertItem?.subtitle, L10n.leaveRoomAlertPrivateSubtitle) + + try await deferred.fulfill() + + XCTAssertEqual(context.viewState.bindings.leaveRoomAlertItem?.state, .private) + XCTAssertEqual(context.viewState.bindings.leaveRoomAlertItem?.subtitle, L10n.leaveRoomAlertPrivateSubtitle) } func testLeaveRoomTappedWithLessThanTwoMembers() async { @@ -82,24 +87,24 @@ class RoomDetailsScreenViewModelTests: XCTestCase { XCTAssertEqual(context.leaveRoomAlertItem?.subtitle, L10n.leaveRoomAlertEmptySubtitle) } - func testLeaveRoomSuccess() async { - let expectation = expectation(description: #function) + func testLeaveRoomSuccess() async throws { roomProxyMock.leaveRoomClosure = { .success(()) } - viewModel.actions - .sink { action in - switch action { - case .leftRoom: - break - default: - XCTFail("leftRoom expected") - } - expectation.fulfill() + + let deferred = deferFulfillment(viewModel.actions) { action in + switch action { + case .leftRoom: + return true + default: + return false } - .store(in: &cancellables) + } + context.send(viewAction: .confirmLeave) - await fulfillment(of: [expectation]) + + try await deferred.fulfill() + XCTAssertEqual(roomProxyMock.leaveRoomCallsCount, 1) } @@ -117,7 +122,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { XCTAssertNotNil(context.alertInfo) } - func testInitialDMDetailsState() async { + func testInitialDMDetailsState() async throws { let recipient = RoomMemberProxyMock.mockDan let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient] roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers, activeMembersCount: mockedMembers.count)) @@ -126,7 +131,13 @@ class RoomDetailsScreenViewModelTests: XCTestCase { mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration())) - await context.nextViewState() + + let deferred = deferFulfillment(viewModel.context.$viewState) { state in + state.dmRecipient != nil + } + + try await deferred.fulfill() + XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient)) } @@ -136,6 +147,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { try? await Task.sleep(for: .milliseconds(100)) return .success(()) } + let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient] roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers, activeMembersCount: mockedMembers.count)) viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", @@ -143,16 +155,23 @@ class RoomDetailsScreenViewModelTests: XCTestCase { mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration())) - await context.nextViewState() + + var deferred = deferFulfillment(viewModel.context.$viewState) { state in + state.dmRecipient != nil + } + + try await deferred.fulfill() + XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient)) - let deferred = deferFulfillment(context.$viewState.map(\.isProcessingIgnoreRequest) - .removeDuplicates() - .collect(3).first()) + deferred = deferFulfillment(viewModel.context.$viewState, + keyPath: \.isProcessingIgnoreRequest, + transitionValues: [false, true, false]) + context.send(viewAction: .ignoreConfirmed) - let states = try await deferred.fulfill() - XCTAssertEqual(states, [false, true, false]) + try await deferred.fulfill() + XCTAssert(context.viewState.dmRecipient?.isIgnored == true) } @@ -169,16 +188,23 @@ class RoomDetailsScreenViewModelTests: XCTestCase { mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration())) - await context.nextViewState() + + var deferred = deferFulfillment(viewModel.context.$viewState) { state in + state.dmRecipient != nil + } + + try await deferred.fulfill() + XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient)) - let deferred = deferFulfillment(context.$viewState.map(\.isProcessingIgnoreRequest) - .removeDuplicates() - .collect(3).first()) + deferred = deferFulfillment(viewModel.context.$viewState, + keyPath: \.isProcessingIgnoreRequest, + transitionValues: [false, true, false]) + context.send(viewAction: .ignoreConfirmed) - let states = try await deferred.fulfill() - XCTAssertEqual(states, [false, true, false]) + try await deferred.fulfill() + XCTAssert(context.viewState.dmRecipient?.isIgnored == false) XCTAssertNotNil(context.alertInfo) } @@ -196,16 +222,23 @@ class RoomDetailsScreenViewModelTests: XCTestCase { mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration())) - await context.nextViewState() + + var deferred = deferFulfillment(viewModel.context.$viewState) { state in + state.dmRecipient != nil + } + + try await deferred.fulfill() + XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient)) - let deferred = deferFulfillment(context.$viewState.map(\.isProcessingIgnoreRequest) - .removeDuplicates() - .collect(3).first()) + deferred = deferFulfillment(viewModel.context.$viewState, + keyPath: \.isProcessingIgnoreRequest, + transitionValues: [false, true, false]) context.send(viewAction: .unignoreConfirmed) - let states = try await deferred.fulfill() - XCTAssertEqual(states, [false, true, false]) + + try await deferred.fulfill() + XCTAssert(context.viewState.dmRecipient?.isIgnored == false) } @@ -222,16 +255,23 @@ class RoomDetailsScreenViewModelTests: XCTestCase { mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration())) - await context.nextViewState() + + var deferred = deferFulfillment(viewModel.context.$viewState) { state in + state.dmRecipient != nil + } + + try await deferred.fulfill() + XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient)) - let deferred = deferFulfillment(context.$viewState.map(\.isProcessingIgnoreRequest) - .removeDuplicates() - .collect(3).first()) + deferred = deferFulfillment(viewModel.context.$viewState, + keyPath: \.isProcessingIgnoreRequest, + transitionValues: [false, true, false]) context.send(viewAction: .unignoreConfirmed) - let states = try await deferred.fulfill() - XCTAssertEqual(states, [false, true, false]) + + try await deferred.fulfill() + XCTAssert(context.viewState.dmRecipient?.isIgnored == true) XCTAssertNotNil(context.alertInfo) } @@ -283,7 +323,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { await Task.yield() XCTAssertTrue(callbackCorrectlyCalled) } - + func testCanEditAvatar() async { let owner: RoomMemberProxyMock = .mockOwner(allowedStateEvents: [.roomAvatar]) let mockedMembers: [RoomMemberProxyMock] = [owner, .mockBob, .mockAlice] @@ -379,10 +419,19 @@ class RoomDetailsScreenViewModelTests: XCTestCase { mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: notificationSettingsProxyMock) - let deferred = deferFulfillment(context.$viewState.map(\.notificationSettingsState) - .filter(\.isError) - .first()) + + var deferred = deferFulfillment(context.$viewState) { state in + state.notificationSettingsState.isError + } + + try await deferred.fulfill() + notificationSettingsProxyMock.callbacks.send(.settingsDidChange) + + deferred = deferFulfillment(context.$viewState) { state in + state.notificationSettingsState.isError + } + try await deferred.fulfill() let expectedAlertInfo = AlertInfo(id: RoomDetailsScreenErrorType.alert, @@ -395,28 +444,40 @@ class RoomDetailsScreenViewModelTests: XCTestCase { func testNotificationDefaultMode() async throws { notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .allMessages, isDefault: true)) - let deferred = deferFulfillment(context.$viewState.map(\.notificationSettingsState).first(where: \.isLoaded)) + + let deferred = deferFulfillment(context.$viewState) { state in + state.notificationSettingsState.isLoaded + } + notificationSettingsProxyMock.callbacks.send(.settingsDidChange) try await deferred.fulfill() - + XCTAssertEqual(context.viewState.notificationSettingsState.label, "Default") } func testNotificationCustomMode() async throws { notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .allMessages, isDefault: false)) - let deferred = deferFulfillment(context.$viewState.map(\.notificationSettingsState).first(where: \.isCustom)) + + let deferred = deferFulfillment(context.$viewState) { state in + state.notificationSettingsState.isCustom + } + notificationSettingsProxyMock.callbacks.send(.settingsDidChange) try await deferred.fulfill() - + XCTAssertEqual(context.viewState.notificationSettingsState.label, "Custom") } func testNotificationRoomMuted() async throws { notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mute, isDefault: false)) - let deferred = deferFulfillment(context.$viewState.map(\.notificationSettingsState).first(where: \.isLoaded)) + + let deferred = deferFulfillment(context.$viewState) { state in + state.notificationSettingsState.isLoaded + } + notificationSettingsProxyMock.callbacks.send(.settingsDidChange) try await deferred.fulfill() - + _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first() XCTAssertEqual(context.viewState.notificationShortcutButtonTitle, L10n.commonUnmute) @@ -425,10 +486,14 @@ class RoomDetailsScreenViewModelTests: XCTestCase { func testNotificationRoomNotMuted() async throws { notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mentionsAndKeywordsOnly, isDefault: false)) - let deferred = deferFulfillment(context.$viewState.map(\.notificationSettingsState).first(where: \.isLoaded)) + + let deferred = deferFulfillment(context.$viewState) { state in + state.notificationSettingsState.isLoaded + } + notificationSettingsProxyMock.callbacks.send(.settingsDidChange) try await deferred.fulfill() - + XCTAssertEqual(context.viewState.notificationShortcutButtonTitle, L10n.commonMute) XCTAssertEqual(context.viewState.notificationShortcutButtonImage, Image(systemName: "bell")) } @@ -485,7 +550,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { func testMuteTapped() async throws { try await testNotificationRoomNotMuted() - + let expectation = expectation(description: #function) notificationSettingsProxyMock.setNotificationModeRoomIdModeClosure = { [weak notificationSettingsProxyMock] _, mode in notificationSettingsProxyMock?.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: mode, isDefault: false)) @@ -495,9 +560,12 @@ class RoomDetailsScreenViewModelTests: XCTestCase { await fulfillment(of: [expectation]) XCTAssertFalse(context.viewState.isProcessingMuteToggleAction) - + do { - let deferred = deferFulfillment(context.$viewState.first()) + let deferred = deferFulfillment(context.$viewState) { state in + state.notificationSettingsState.isLoaded + } + notificationSettingsProxyMock.callbacks.send(.settingsDidChange) try await deferred.fulfill() } @@ -512,7 +580,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { func testUnmuteTapped() async throws { try await testNotificationRoomMuted() - + let expectation = expectation(description: #function) notificationSettingsProxyMock.unmuteRoomRoomIdIsEncryptedIsOneToOneClosure = { [weak notificationSettingsProxyMock] _, _, _ in notificationSettingsProxyMock?.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .allMessages, isDefault: false)) @@ -524,7 +592,10 @@ class RoomDetailsScreenViewModelTests: XCTestCase { XCTAssertFalse(context.viewState.isProcessingMuteToggleAction) do { - let deferred = deferFulfillment(context.$viewState.first()) + let deferred = deferFulfillment(context.$viewState) { state in + state.notificationSettingsState.isLoaded + } + notificationSettingsProxyMock.callbacks.send(.settingsDidChange) try await deferred.fulfill() } diff --git a/UnitTests/Sources/RoomFlowCoordinatorTests.swift b/UnitTests/Sources/RoomFlowCoordinatorTests.swift index 991201e3c..c15d5e940 100644 --- a/UnitTests/Sources/RoomFlowCoordinatorTests.swift +++ b/UnitTests/Sources/RoomFlowCoordinatorTests.swift @@ -119,17 +119,22 @@ class RoomFlowCoordinatorTests: XCTestCase { } private func process(route: AppRoute, expectedActions: [RoomFlowCoordinatorAction]) async throws { - let deferred = deferFulfillment(roomFlowCoordinator.actions.collect(expectedActions.count).first(), - message: "The expected number of actions should be published.") - - Task { - await Task.yield() - self.roomFlowCoordinator.handleAppRoute(route, animated: true) + guard !expectedActions.isEmpty else { + return } - if !expectedActions.isEmpty { - let actions = try await deferred.fulfill() - XCTAssertEqual(actions, expectedActions) + var fulfillments = [DeferredFulfillment]() + + for expectedAction in expectedActions { + fulfillments.append(deferFulfillment(roomFlowCoordinator.actions) { action in + action == expectedAction + }) + } + + roomFlowCoordinator.handleAppRoute(route, animated: true) + + for fulfillment in fulfillments { + try await fulfillment.fulfill() } } } diff --git a/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift b/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift index 279d00ec0..0e2a04122 100644 --- a/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift +++ b/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift @@ -55,16 +55,17 @@ class RoomMemberDetailsViewModelTests: XCTestCase { context.send(viewAction: .showIgnoreAlert) XCTAssertEqual(context.ignoreUserAlert, .init(action: .ignore)) - let deferred = deferFulfillment(context.$viewState.map(\.isProcessingIgnoreRequest) - .removeDuplicates() - .collect(3).first()) - context.send(viewAction: .ignoreConfirmed) - let states = try await deferred.fulfill() - XCTAssertEqual(states, [false, true, false]) + + let deferred = deferFulfillment(context.$viewState) { state in + state.details.isIgnored + } + + try await deferred.fulfill() + XCTAssertFalse(context.viewState.isProcessingIgnoreRequest) XCTAssertTrue(context.viewState.details.isIgnored) - try await Task.sleep(for: .microseconds(100)) + try await Task.sleep(for: .milliseconds(100)) XCTAssertTrue(roomProxyMock.updateMembersCalled) } @@ -80,17 +81,18 @@ class RoomMemberDetailsViewModelTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController) context.send(viewAction: .showIgnoreAlert) XCTAssertEqual(context.ignoreUserAlert, .init(action: .ignore)) - - let deferred = deferFulfillment(context.$viewState.map(\.isProcessingIgnoreRequest) - .removeDuplicates() - .collect(3).first()) context.send(viewAction: .ignoreConfirmed) - let states = try await deferred.fulfill() - XCTAssertEqual(states, [false, true, false]) + + let deferred = deferFulfillment(context.$viewState) { state in + state.bindings.alertInfo != nil + } + + try await deferred.fulfill() + XCTAssertNotNil(context.alertInfo) XCTAssertFalse(context.viewState.details.isIgnored) - try await Task.sleep(for: .microseconds(100)) + try await Task.sleep(for: .milliseconds(100)) XCTAssertFalse(roomProxyMock.updateMembersCalled) } @@ -107,16 +109,17 @@ class RoomMemberDetailsViewModelTests: XCTestCase { context.send(viewAction: .showUnignoreAlert) XCTAssertEqual(context.ignoreUserAlert, .init(action: .unignore)) - - let deferred = deferFulfillment(context.$viewState.map(\.isProcessingIgnoreRequest) - .removeDuplicates() - .collect(3).first()) context.send(viewAction: .unignoreConfirmed) - let states = try await deferred.fulfill() - XCTAssertEqual(states, [false, true, false]) + + let deferred = deferFulfillment(context.$viewState) { state in + state.details.isIgnored == false + } + + try await deferred.fulfill() + XCTAssertFalse(context.viewState.details.isIgnored) - try await Task.sleep(for: .microseconds(100)) + try await Task.sleep(for: .milliseconds(100)) XCTAssertTrue(roomProxyMock.updateMembersCalled) } @@ -134,17 +137,18 @@ class RoomMemberDetailsViewModelTests: XCTestCase { context.send(viewAction: .showUnignoreAlert) XCTAssertEqual(context.ignoreUserAlert, .init(action: .unignore)) - - let deferred = deferFulfillment(context.$viewState.map(\.isProcessingIgnoreRequest) - .removeDuplicates() - .collect(3).first()) context.send(viewAction: .unignoreConfirmed) - let states = try await deferred.fulfill() - XCTAssertEqual(states, [false, true, false]) + + let deferred = deferFulfillment(context.$viewState) { state in + state.bindings.alertInfo != nil + } + + try await deferred.fulfill() + XCTAssertTrue(context.viewState.details.isIgnored) XCTAssertNotNil(context.alertInfo) - try await Task.sleep(for: .microseconds(100)) + try await Task.sleep(for: .milliseconds(100)) XCTAssertFalse(roomProxyMock.updateMembersCalled) } diff --git a/UnitTests/Sources/RoomMembersListViewModelTests.swift b/UnitTests/Sources/RoomMembersListViewModelTests.swift index bfb746f5b..0c360a273 100644 --- a/UnitTests/Sources/RoomMembersListViewModelTests.swift +++ b/UnitTests/Sources/RoomMembersListViewModelTests.swift @@ -26,49 +26,87 @@ class RoomMembersListScreenViewModelTests: XCTestCase { viewModel.context } - func testJoinedMembers() async { + func testJoinedMembers() async throws { setup(with: [.mockAlice, .mockBob]) - await context.nextViewState() + + let deferred = deferFulfillment(context.$viewState) { state in + state.visibleJoinedMembers.count == 2 + } + + try await deferred.fulfill() + XCTAssertEqual(viewModel.state.joinedMembersCount, 2) XCTAssertEqual(viewModel.state.visibleJoinedMembers.count, 2) } - func testSearch() async { + func testSearch() async throws { setup(with: [.mockAlice, .mockBob]) - await context.nextViewState() + + let deferred = deferFulfillment(context.$viewState) { state in + state.visibleJoinedMembers.count == 1 + } + context.searchQuery = "alice" + + try await deferred.fulfill() + XCTAssertEqual(viewModel.state.joinedMembersCount, 2) XCTAssertEqual(viewModel.state.visibleJoinedMembers.count, 1) } - func testEmptySearch() async { + func testEmptySearch() async throws { setup(with: [.mockAlice, .mockBob]) - await context.nextViewState() context.searchQuery = "WWW" + + let deferred = deferFulfillment(context.$viewState) { state in + state.joinedMembersCount == 2 + } + + try await deferred.fulfill() + XCTAssertEqual(viewModel.state.joinedMembersCount, 2) XCTAssertEqual(viewModel.state.visibleJoinedMembers.count, 0) } - func testJoinedAndInvitedMembers() async { + func testJoinedAndInvitedMembers() async throws { setup(with: [.mockInvitedAlice, .mockBob]) - await context.nextViewState() + + let deferred = deferFulfillment(context.$viewState) { state in + state.visibleInvitedMembers.count == 1 + } + + try await deferred.fulfill() + XCTAssertEqual(viewModel.state.joinedMembersCount, 1) XCTAssertEqual(viewModel.state.visibleInvitedMembers.count, 1) XCTAssertEqual(viewModel.state.visibleJoinedMembers.count, 1) } - func testInvitedMembers() async { + func testInvitedMembers() async throws { setup(with: [.mockInvitedAlice]) - await context.nextViewState() + + let deferred = deferFulfillment(context.$viewState) { state in + state.visibleInvitedMembers.count == 1 + } + + try await deferred.fulfill() + XCTAssertEqual(viewModel.state.joinedMembersCount, 0) XCTAssertEqual(viewModel.state.visibleInvitedMembers.count, 1) XCTAssertEqual(viewModel.state.visibleJoinedMembers.count, 0) } - func testSearchInvitedMembers() async { + func testSearchInvitedMembers() async throws { setup(with: [.mockInvitedAlice]) + context.searchQuery = "alice" - await context.nextViewState() + + let deferred = deferFulfillment(context.$viewState) { state in + state.visibleInvitedMembers.count == 1 + } + + try await deferred.fulfill() + XCTAssertEqual(viewModel.state.joinedMembersCount, 0) XCTAssertEqual(viewModel.state.visibleInvitedMembers.count, 1) XCTAssertEqual(viewModel.state.visibleJoinedMembers.count, 0) diff --git a/UnitTests/Sources/RoomNotificationSettingsScreenViewModelTests.swift b/UnitTests/Sources/RoomNotificationSettingsScreenViewModelTests.swift index a7053cd55..3fd62f7e4 100644 --- a/UnitTests/Sources/RoomNotificationSettingsScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomNotificationSettingsScreenViewModelTests.swift @@ -22,99 +22,118 @@ import XCTest @MainActor class RoomNotificationSettingsScreenViewModelTests: XCTestCase { - var viewModel: RoomNotificationSettingsScreenViewModel! var roomProxyMock: RoomProxyMock! var notificationSettingsProxyMock: NotificationSettingsProxyMock! - var context: RoomNotificationSettingsScreenViewModelType.Context { viewModel.context } var cancellables = Set() override func setUpWithError() throws { cancellables.removeAll() roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", joinedMembersCount: 0)) notificationSettingsProxyMock = NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()) - viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, - roomProxy: roomProxyMock, - displayAsUserDefinedRoomSettings: false) } - + func testInitialStateDefaultMode() async throws { + let roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", joinedMembersCount: 0)) + let notificationSettingsProxyMock = NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()) + notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mentionsAndKeywordsOnly, isDefault: true)) - viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, - roomProxy: roomProxyMock, - displayAsUserDefinedRoomSettings: false) - let deferred = deferFulfillment(context.$viewState.map(\.notificationSettingsState) - .first(where: \.isLoaded)) + + let viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, + roomProxy: roomProxyMock, + displayAsUserDefinedRoomSettings: false) + + let deferred = deferFulfillment(viewModel.context.$viewState) { state in + state.notificationSettingsState.isLoaded + } + notificationSettingsProxyMock.callbacks.send(.settingsDidChange) try await deferred.fulfill() - XCTAssertFalse(context.allowCustomSetting) + XCTAssertFalse(viewModel.context.allowCustomSetting) } func testInitialStateCustomMode() async throws { notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mentionsAndKeywordsOnly, isDefault: false)) - viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, - roomProxy: roomProxyMock, - displayAsUserDefinedRoomSettings: false) - let deferred = deferFulfillment(context.$viewState.map(\.notificationSettingsState) - .first(where: \.isLoaded)) + let viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, + roomProxy: roomProxyMock, + displayAsUserDefinedRoomSettings: false) + let deferred = deferFulfillment(viewModel.context.$viewState) { state in + state.notificationSettingsState.isLoaded + } + notificationSettingsProxyMock.callbacks.send(.settingsDidChange) try await deferred.fulfill() - XCTAssertTrue(context.allowCustomSetting) + XCTAssertTrue(viewModel.context.allowCustomSetting) } func testInitialStateFailure() async throws { notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneThrowableError = NotificationSettingsError.Generic(message: "error") - viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, - roomProxy: roomProxyMock, - displayAsUserDefinedRoomSettings: false) - let deferred = deferFulfillment(context.$viewState.map(\.notificationSettingsState) - .first(where: \.isError)) + let viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, + roomProxy: roomProxyMock, + displayAsUserDefinedRoomSettings: false) + let deferred = deferFulfillment(viewModel.context.$viewState) { state in + state.notificationSettingsState.isError + } + notificationSettingsProxyMock.callbacks.send(.settingsDidChange) try await deferred.fulfill() let expectedAlertInfo = AlertInfo(id: RoomNotificationSettingsScreenErrorType.loadingSettingsFailed, title: L10n.commonError, message: L10n.screenRoomNotificationSettingsErrorLoadingSettings) - XCTAssertEqual(context.viewState.bindings.alertInfo?.id, expectedAlertInfo.id) - XCTAssertEqual(context.viewState.bindings.alertInfo?.title, expectedAlertInfo.title) - XCTAssertEqual(context.viewState.bindings.alertInfo?.message, expectedAlertInfo.message) + XCTAssertEqual(viewModel.context.viewState.bindings.alertInfo?.id, expectedAlertInfo.id) + XCTAssertEqual(viewModel.context.viewState.bindings.alertInfo?.title, expectedAlertInfo.title) + XCTAssertEqual(viewModel.context.viewState.bindings.alertInfo?.message, expectedAlertInfo.message) } func testToggleAllCustomSettingOff() async throws { notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mentionsAndKeywordsOnly, isDefault: false)) - viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, - roomProxy: roomProxyMock, - displayAsUserDefinedRoomSettings: false) - let deferred = deferFulfillment(context.$viewState.map(\.notificationSettingsState) - .first(where: \.isLoaded)) + let viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, + roomProxy: roomProxyMock, + displayAsUserDefinedRoomSettings: false) + let deferred = deferFulfillment(viewModel.context.$viewState) { state in + state.notificationSettingsState.isLoaded + } + notificationSettingsProxyMock.callbacks.send(.settingsDidChange) try await deferred.fulfill() + + let deferredIsRestoringDefaultSettings = deferFulfillment(viewModel.context.$viewState, + keyPath: \.isRestoringDefaultSetting, + transitionValues: [false, true, false]) - let deferredIsRestoringDefaultSettings = deferFulfillment(context.$viewState.map(\.isRestoringDefaultSetting) - .removeDuplicates() - .collect(3).first()) viewModel.state.bindings.allowCustomSetting = false - context.send(viewAction: .changedAllowCustomSettings) - let states = try await deferredIsRestoringDefaultSettings.fulfill() - XCTAssertEqual(states, [false, true, false]) + viewModel.context.send(viewAction: .changedAllowCustomSettings) + + try await deferredIsRestoringDefaultSettings.fulfill() XCTAssertEqual(notificationSettingsProxyMock.restoreDefaultNotificationModeRoomIdReceivedRoomId, roomProxyMock.id) XCTAssertEqual(notificationSettingsProxyMock.restoreDefaultNotificationModeRoomIdCallsCount, 1) } func testToggleAllCustomSettingOffOn() async throws { + let notificationSettingsProxyMock = NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()) notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mentionsAndKeywordsOnly, isDefault: true)) - viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, - roomProxy: roomProxyMock, - displayAsUserDefinedRoomSettings: false) - var deferred = deferFulfillment(context.$viewState.map(\.notificationSettingsState).first(where: \.isLoaded)) + let viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, + roomProxy: roomProxyMock, + displayAsUserDefinedRoomSettings: false) + + var deferred = deferFulfillment(viewModel.context.$viewState) { state in + state.notificationSettingsState.isLoaded + } + notificationSettingsProxyMock.callbacks.send(.settingsDidChange) + try await deferred.fulfill() - deferred = deferFulfillment(context.$viewState.map(\.notificationSettingsState).first(where: \.isLoaded)) + deferred = deferFulfillment(viewModel.context.$viewState) { state in + state.notificationSettingsState.isLoaded + } + viewModel.state.bindings.allowCustomSetting = true - context.send(viewAction: .changedAllowCustomSettings) + viewModel.context.send(viewAction: .changedAllowCustomSettings) + try await deferred.fulfill() XCTAssertEqual(notificationSettingsProxyMock.setNotificationModeRoomIdModeReceivedArguments?.0, roomProxyMock.id) @@ -124,17 +143,25 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase { func testSetCustomMode() async throws { notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mentionsAndKeywordsOnly, isDefault: false)) - viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, - roomProxy: roomProxyMock, - displayAsUserDefinedRoomSettings: false) - let deferredState = deferFulfillment(context.$viewState.map(\.notificationSettingsState).first(where: \.isLoaded)) + let viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, + roomProxy: roomProxyMock, + displayAsUserDefinedRoomSettings: false) + + let deferredState = deferFulfillment(viewModel.context.$viewState) { state in + state.notificationSettingsState.isLoaded + } + notificationSettingsProxyMock.callbacks.send(.settingsDidChange) try await deferredState.fulfill() do { - let deferredViewState = deferFulfillment(context.$viewState.collect(2).first()) - context.send(viewAction: .setCustomMode(.allMessages)) - try await deferredViewState.fulfill() + viewModel.context.send(viewAction: .setCustomMode(.allMessages)) + + let deferredState = deferFulfillment(viewModel.context.$viewState) { state in + state.pendingCustomMode == nil + } + + try await deferredState.fulfill() XCTAssertEqual(notificationSettingsProxyMock.setNotificationModeRoomIdModeReceivedArguments?.0, roomProxyMock.id) XCTAssertEqual(notificationSettingsProxyMock.setNotificationModeRoomIdModeReceivedArguments?.1, .allMessages) @@ -142,9 +169,13 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase { } do { - let deferredViewState = deferFulfillment(context.$viewState.collect(2).first()) - context.send(viewAction: .setCustomMode(.mute)) - try await deferredViewState.fulfill() + viewModel.context.send(viewAction: .setCustomMode(.mute)) + + let deferredState = deferFulfillment(viewModel.context.$viewState) { state in + state.pendingCustomMode == nil + } + + try await deferredState.fulfill() XCTAssertEqual(notificationSettingsProxyMock.setNotificationModeRoomIdModeReceivedArguments?.0, roomProxyMock.id) XCTAssertEqual(notificationSettingsProxyMock.setNotificationModeRoomIdModeReceivedArguments?.1, .mute) @@ -152,9 +183,13 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase { } do { - let deferredViewState = deferFulfillment(context.$viewState.collect(2).first()) - context.send(viewAction: .setCustomMode(.mentionsAndKeywordsOnly)) - try await deferredViewState.fulfill() + viewModel.context.send(viewAction: .setCustomMode(.mentionsAndKeywordsOnly)) + + let deferredState = deferFulfillment(viewModel.context.$viewState) { state in + state.pendingCustomMode == nil + } + + try await deferredState.fulfill() XCTAssertEqual(notificationSettingsProxyMock.setNotificationModeRoomIdModeReceivedArguments?.0, roomProxyMock.id) XCTAssertEqual(notificationSettingsProxyMock.setNotificationModeRoomIdModeReceivedArguments?.1, .mentionsAndKeywordsOnly) @@ -164,12 +199,15 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase { func testDeleteCustomSettingTapped() async throws { notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mentionsAndKeywordsOnly, isDefault: false)) - viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, - roomProxy: roomProxyMock, - displayAsUserDefinedRoomSettings: true) - let deferredState = deferFulfillment(context.$viewState.map(\.notificationSettingsState).first(where: \.isLoaded)) + let viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, + roomProxy: roomProxyMock, + displayAsUserDefinedRoomSettings: true) + let deferred = deferFulfillment(viewModel.context.$viewState) { state in + state.notificationSettingsState.isLoaded + } + notificationSettingsProxyMock.callbacks.send(.settingsDidChange) - try await deferredState.fulfill() + try await deferred.fulfill() var actionSent: RoomNotificationSettingsScreenViewModelAction? viewModel.actions @@ -178,33 +216,35 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase { } .store(in: &cancellables) - let deferredViewState = deferFulfillment(context.$viewState - .map(\.deletingCustomSetting) - .removeDuplicates() - .collect(3).first()) - context.send(viewAction: .deleteCustomSettingTapped) - let states = try await deferredViewState.fulfill() + let deferredViewState = deferFulfillment(viewModel.context.$viewState, + keyPath: \.deletingCustomSetting, + transitionValues: [false, true, false]) + + viewModel.context.send(viewAction: .deleteCustomSettingTapped) + + try await deferredViewState.fulfill() - // `deletingCustomSetting` must be set to `true` when deleting, and reset to `false` afterwards. - XCTAssertEqual(states, [false, true, false]) // the `dismiss` action must have been sent XCTAssertEqual(actionSent, .dismiss) // `restoreDefaultNotificationMode` should have been called XCTAssert(notificationSettingsProxyMock.restoreDefaultNotificationModeRoomIdCalled) XCTAssertEqual(notificationSettingsProxyMock.restoreDefaultNotificationModeRoomIdReceivedInvocations, [roomProxyMock.id]) // and no alert is expected - XCTAssertNil(context.alertInfo) + XCTAssertNil(viewModel.context.alertInfo) } func testDeleteCustomSettingTappedFailure() async throws { notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mentionsAndKeywordsOnly, isDefault: false)) notificationSettingsProxyMock.restoreDefaultNotificationModeRoomIdThrowableError = NotificationSettingsError.Generic(message: "error") - viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, - roomProxy: roomProxyMock, - displayAsUserDefinedRoomSettings: true) - let deferredState = deferFulfillment(context.$viewState.map(\.notificationSettingsState).first(where: \.isLoaded)) + let viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, + roomProxy: roomProxyMock, + displayAsUserDefinedRoomSettings: true) + let deferred = deferFulfillment(viewModel.context.$viewState) { state in + state.notificationSettingsState.isLoaded + } + notificationSettingsProxyMock.callbacks.send(.settingsDidChange) - try await deferredState.fulfill() + try await deferred.fulfill() var actionSent: RoomNotificationSettingsScreenViewModelAction? viewModel.actions @@ -213,17 +253,16 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase { } .store(in: &cancellables) - let deferredViewState = deferFulfillment(context.$viewState - .map(\.deletingCustomSetting) - .removeDuplicates() - .collect(3).first()) - context.send(viewAction: .deleteCustomSettingTapped) - let states = try await deferredViewState.fulfill() + let deferredViewState = deferFulfillment(viewModel.context.$viewState, + keyPath: \.deletingCustomSetting, + transitionValues: [false, true, false]) - // `deletingCustomSetting` must be set to `true` when deleting, and reset to `false` afterwards. - XCTAssertEqual(states, [false, true, false]) + viewModel.context.send(viewAction: .deleteCustomSettingTapped) + + try await deferredViewState.fulfill() + // an alert is expected - XCTAssertEqual(context.alertInfo?.id, .restoreDefaultFailed) + XCTAssertEqual(viewModel.context.alertInfo?.id, .restoreDefaultFailed) // the `dismiss` action must not have been sent XCTAssertNil(actionSent) } diff --git a/UnitTests/Sources/RoomScreenViewModelTests.swift b/UnitTests/Sources/RoomScreenViewModelTests.swift index 014fb80d9..c6d9f395d 100644 --- a/UnitTests/Sources/RoomScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomScreenViewModelTests.swift @@ -282,11 +282,14 @@ class RoomScreenViewModelTests: XCTestCase { } .store(in: &cancellables) - // Test - let deferred = deferFulfillment(viewModel.context.$viewState.collect(3).first(), - message: "The existing view state plus one new one should be published.") + let deferred = deferFulfillment(viewModel.context.$viewState) { value in + value.bindings.alertInfo != nil + } + viewModel.context.send(viewAction: .tappedOnUser(userID: "bob")) + try await deferred.fulfill() + XCTAssertFalse(viewModel.state.bindings.alertInfo.isNil) XCTAssert(roomProxyMock.getMemberUserIDCallsCount == 1) XCTAssertEqual(roomProxyMock.getMemberUserIDReceivedUserID, "bob") @@ -295,7 +298,6 @@ class RoomScreenViewModelTests: XCTestCase { // MARK: - Sending func testRetrySend() async throws { - // Setup let timelineController = MockRoomTimelineController() let roomProxyMock = RoomProxyMock(with: .init(displayName: "")) @@ -306,16 +308,15 @@ class RoomScreenViewModelTests: XCTestCase { analytics: ServiceLocator.shared.analytics, userIndicatorController: userIndicatorControllerMock) - // Test viewModel.context.send(viewAction: .retrySend(itemID: .init(timelineID: UUID().uuidString, transactionID: "test retry send id"))) - await Task.yield() - try? await Task.sleep(for: .microseconds(500)) + + try? await Task.sleep(for: .milliseconds(100)) + XCTAssert(roomProxyMock.retrySendTransactionIDCallsCount == 1) XCTAssert(roomProxyMock.retrySendTransactionIDReceivedInvocations == ["test retry send id"]) } func testRetrySendNoTransactionID() async { - // Setup let timelineController = MockRoomTimelineController() let roomProxyMock = RoomProxyMock(with: .init(displayName: "")) @@ -326,14 +327,14 @@ class RoomScreenViewModelTests: XCTestCase { analytics: ServiceLocator.shared.analytics, userIndicatorController: userIndicatorControllerMock) - // Test viewModel.context.send(viewAction: .retrySend(itemID: .random)) - await Task.yield() + + try? await Task.sleep(for: .milliseconds(100)) + XCTAssert(roomProxyMock.retrySendTransactionIDCallsCount == 0) } func testCancelSend() async { - // Setup let timelineController = MockRoomTimelineController() let roomProxyMock = RoomProxyMock(with: .init(displayName: "")) @@ -344,15 +345,15 @@ class RoomScreenViewModelTests: XCTestCase { analytics: ServiceLocator.shared.analytics, userIndicatorController: userIndicatorControllerMock) - // Test viewModel.context.send(viewAction: .cancelSend(itemID: .init(timelineID: UUID().uuidString, transactionID: "test cancel send id"))) - try? await Task.sleep(for: .microseconds(500)) + + try? await Task.sleep(for: .milliseconds(100)) + XCTAssert(roomProxyMock.cancelSendTransactionIDCallsCount == 1) XCTAssert(roomProxyMock.cancelSendTransactionIDReceivedInvocations == ["test cancel send id"]) } func testCancelSendNoTransactionID() async { - // Setup let timelineController = MockRoomTimelineController() let roomProxyMock = RoomProxyMock(with: .init(displayName: "")) @@ -363,16 +364,16 @@ class RoomScreenViewModelTests: XCTestCase { analytics: ServiceLocator.shared.analytics, userIndicatorController: userIndicatorControllerMock) - // Test viewModel.context.send(viewAction: .cancelSend(itemID: .random)) - await Task.yield() + + try? await Task.sleep(for: .milliseconds(100)) + XCTAssert(roomProxyMock.cancelSendTransactionIDCallsCount == 0) } // MARK: - Read Receipts // swiftlint:disable force_unwrapping - func testSendReadReceipt() async throws { // Given a room with only text items in the timeline let items = [TextRoomTimelineItem(eventID: "t1"), @@ -382,7 +383,7 @@ class RoomScreenViewModelTests: XCTestCase { // When sending a read receipt for the last item. viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.last!.id)) - try await Task.sleep(for: .microseconds(100)) + try await Task.sleep(for: .milliseconds(100)) // Then the receipt should be sent. XCTAssertEqual(roomProxy.sendReadReceiptForCalled, true) @@ -401,13 +402,13 @@ class RoomScreenViewModelTests: XCTestCase { TextRoomTimelineItem(eventID: "t3")] let (viewModel, roomProxy, timelineController, _) = readReceiptsConfiguration(with: items) viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.last!.id)) - try await Task.sleep(for: .microseconds(100)) + try await Task.sleep(for: .milliseconds(100)) XCTAssertEqual(roomProxy.sendReadReceiptForCallsCount, 1) XCTAssertEqual(roomProxy.sendReadReceiptForReceivedEventID, "t3") // When sending a receipt for the first item in the timeline. viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.first!.id)) - try await Task.sleep(for: .microseconds(100)) + try await Task.sleep(for: .milliseconds(100)) // Then the request should be ignored. XCTAssertEqual(roomProxy.sendReadReceiptForCallsCount, 1) @@ -417,10 +418,10 @@ class RoomScreenViewModelTests: XCTestCase { let newMessage = TextRoomTimelineItem(eventID: "t4") timelineController.timelineItems.append(newMessage) timelineController.callbacks.send(.updatedTimelineItems) - try await Task.sleep(for: .microseconds(500)) + try await Task.sleep(for: .milliseconds(100)) viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(newMessage.id)) - try await Task.sleep(for: .microseconds(100)) + try await Task.sleep(for: .milliseconds(100)) // Then the request should be made. XCTAssertEqual(roomProxy.sendReadReceiptForCallsCount, 2) @@ -436,7 +437,7 @@ class RoomScreenViewModelTests: XCTestCase { // When sending a read receipt for the last item. viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.last!.id)) - try await Task.sleep(for: .microseconds(100)) + try await Task.sleep(for: .milliseconds(100)) // Then nothing should be sent. XCTAssertEqual(roomProxy.sendReadReceiptForCalled, false) @@ -451,7 +452,7 @@ class RoomScreenViewModelTests: XCTestCase { // When sending a read receipt for the last item. viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.last!.id)) - try await Task.sleep(for: .microseconds(100)) + try await Task.sleep(for: .milliseconds(100)) // Then a read receipt should be sent for the item before it. XCTAssertEqual(roomProxy.sendReadReceiptForCalled, true) @@ -465,13 +466,13 @@ class RoomScreenViewModelTests: XCTestCase { SeparatorRoomTimelineItem(timelineID: "v3")] let (viewModel, roomProxy, _, _) = readReceiptsConfiguration(with: items) viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.last!.id)) - try await Task.sleep(for: .microseconds(100)) + try await Task.sleep(for: .milliseconds(100)) XCTAssertEqual(roomProxy.sendReadReceiptForCallsCount, 1) XCTAssertEqual(roomProxy.sendReadReceiptForReceivedEventID, "t2") // When sending the same receipt again viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.last!.id)) - try await Task.sleep(for: .microseconds(100)) + try await Task.sleep(for: .milliseconds(100)) // Then the second call should be ignored. XCTAssertEqual(roomProxy.sendReadReceiptForCallsCount, 1) diff --git a/UnitTests/Sources/SessionVerificationViewModelTests.swift b/UnitTests/Sources/SessionVerificationViewModelTests.swift index f65677ade..81172814a 100644 --- a/UnitTests/Sources/SessionVerificationViewModelTests.swift +++ b/UnitTests/Sources/SessionVerificationViewModelTests.swift @@ -50,7 +50,11 @@ class SessionVerificationViewModelTests: XCTestCase { XCTAssertEqual(context.viewState.verificationState, .cancelling) - await context.nextViewState() + let deferred = deferFulfillment(context.$viewState) { state in + state.verificationState == .cancelled + } + + try await deferred.fulfill() XCTAssertEqual(context.viewState.verificationState, .cancelled) diff --git a/UnitTests/Sources/StaticLocationScreenViewModelTests.swift b/UnitTests/Sources/StaticLocationScreenViewModelTests.swift index 609ef3621..a8a6625a7 100644 --- a/UnitTests/Sources/StaticLocationScreenViewModelTests.swift +++ b/UnitTests/Sources/StaticLocationScreenViewModelTests.swift @@ -81,7 +81,16 @@ class StaticLocationScreenViewModelTests: XCTestCase { func testSendUserLocation() async throws { context.mapCenterLocation = .init(latitude: 0, longitude: 0) context.geolocationUncertainty = 10 - let deferred = deferFulfillment(viewModel.actions.first()) + + let deferred = deferFulfillment(viewModel.actions) { action in + switch action { + case .sendLocation: + return true + default: + return false + } + } + context.send(viewAction: .selectLocation) guard case .sendLocation(let geoUri, let isUserLocation) = try await deferred.fulfill() else { XCTFail("Sent action should be 'sendLocation'") @@ -95,7 +104,16 @@ class StaticLocationScreenViewModelTests: XCTestCase { context.mapCenterLocation = .init(latitude: 0, longitude: 0) context.isLocationAuthorized = nil context.geolocationUncertainty = 10 - let deferred = deferFulfillment(viewModel.actions.first()) + + let deferred = deferFulfillment(viewModel.actions) { action in + switch action { + case .sendLocation: + return true + default: + return false + } + } + context.send(viewAction: .selectLocation) guard case .sendLocation(let geoUri, let isUserLocation) = try await deferred.fulfill() else { XCTFail("Sent action should be 'sendLocation'") diff --git a/fastlane/Fastfile b/fastlane/Fastfile index ca34288d8..d05025811 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -81,7 +81,6 @@ lane :unit_tests do device: 'iPhone 14 (16.4)', ensure_devices_found: true, result_bundle: true, - number_of_retries: 3, xcargs: '-skipPackagePluginValidation', )