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
This commit is contained in:
Stefan Ceriu 2023-09-26 13:28:29 +03:00 committed by GitHub
parent 1f3898c69d
commit a05c3e3774
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 841 additions and 519 deletions

View File

@ -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 = "<group>"; };
80E815FF3CC5E5A355E3A25E /* RoomMessageEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageEventStringBuilder.swift; sourceTree = "<group>"; };
818695BED971753243FEF897 /* StickerRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerRoomTimelineItem.swift; sourceTree = "<group>"; };
818CBE6249ED6E8FC30E8366 /* ViewModelContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelContext.swift; sourceTree = "<group>"; };
8196D64EB9CF2AF1F43E4ED1 /* AnalyticsPromptScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreenViewModelProtocol.swift; sourceTree = "<group>"; };
81A9B5225D0881CEFA2CF7C9 /* RoomNotificationSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenViewModel.swift; sourceTree = "<group>"; };
81B17B1F29448D1B9049B11C /* ReportContentScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenViewModel.swift; sourceTree = "<group>"; };
@ -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 */,

View File

@ -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)
}
}

View File

@ -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

View File

@ -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()
}

View File

@ -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)

View File

@ -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()

View File

@ -71,17 +71,18 @@ 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 deferred = deferFulfillment(viewModel.actions) { action in
switch action {
case .submitFailed:
return true
default:
return false
}
}
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")
}
guard case .submitFailed = actions[1] else {
return XCTFail("Action 2 was not .submitFailed")
}
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: []))

View File

@ -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 {

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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<T: Publisher>(_ publisher: T, timeout: TimeInterval = 10, message: String? = nil) -> DeferredFulfillment<T.Output> {
var result: Result<T.Output, Error>?
func deferFulfillment<P: Publisher>(_ publisher: P,
timeout: TimeInterval = 10,
message: String? = nil,
until condition: @escaping (P.Output) -> Bool) -> DeferredFulfillment<P.Output> {
var result: Result<P.Output, Error>?
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<T.Output> {
return DeferredFulfillment<P.Output> {
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<P: Publisher, K: KeyPath<P.Output, V>, V: Equatable>(_ publisher: P,
keyPath: K,
transitionValues: [V],
timeout: TimeInterval = 10,
message: String? = nil) -> DeferredFulfillment<P.Output> {
var expectedOrder = transitionValues
let deferred = deferFulfillment<P>(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<T> {
let closure: () async throws -> T
@discardableResult func fulfill() async throws -> T {

View File

@ -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 {

View File

@ -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
}

View File

@ -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()

View File

@ -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())
context.send(viewAction: .setMode(.allMessages))
let pendingModes = try await deferredViewState.fulfill()
var deferredViewState = deferFulfillment(viewModel.context.$viewState, keyPath: \.pendingMode, transitionValues: [nil, .allMessages, nil])
XCTAssertEqual(pendingModes, [nil, .allMessages, nil])
context.send(viewAction: .setMode(.allMessages))
try await deferredViewState.fulfill()
// `setDefaultRoomNotificationModeIsEncryptedIsOneToOneMode` must have been called twice (for encrypted and unencrypted group chats)
let invocations = notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeReceivedInvocations
@ -101,11 +103,11 @@ 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)
@ -115,20 +117,22 @@ 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())
context.send(viewAction: .setMode(.mentionsAndKeywordsOnly))
let pendingModes = try await deferredViewState.fulfill()
var deferredViewState = deferFulfillment(viewModel.context.$viewState,
keyPath: \.pendingMode,
transitionValues: [nil, .mentionsAndKeywordsOnly, nil])
XCTAssertEqual(pendingModes, [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,11 +146,11 @@ 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)
@ -158,20 +162,22 @@ class NotificationSettingsEditScreenViewModelTests: XCTestCase {
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())
context.send(viewAction: .setMode(.allMessages))
let pendingModes = try await deferredViewState.fulfill()
let deferredViewState = deferFulfillment(viewModel.context.$viewState,
keyPath: \.pendingMode,
transitionValues: [nil, .allMessages, nil])
XCTAssertEqual(pendingModes, [nil, .allMessages, nil])
context.send(viewAction: .setMode(.allMessages))
try await deferredViewState.fulfill()
// `setDefaultRoomNotificationModeIsEncryptedIsOneToOneMode` must have been called twice (for encrypted and unencrypted direct chats)
let invocations = notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeReceivedInvocations
@ -192,20 +198,23 @@ class NotificationSettingsEditScreenViewModelTests: XCTestCase {
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())
context.send(viewAction: .setMode(.allMessages))
let pendingModes = try await deferredViewState.fulfill()
let deferredViewState = deferFulfillment(viewModel.context.$viewState,
keyPath: \.pendingMode,
transitionValues: [nil, .allMessages, nil])
context.send(viewAction: .setMode(.allMessages))
try await deferredViewState.fulfill()
XCTAssertEqual(pendingModes, [nil, .allMessages, nil])
XCTAssertNotNil(context.viewState.bindings.alertInfo)
}
@ -215,13 +224,20 @@ class NotificationSettingsEditScreenViewModelTests: XCTestCase {
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
}
}

View File

@ -71,9 +71,13 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
return .mentionsAndKeywordsOnly
}
}
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(notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneCallsCount, 4)
@ -98,9 +102,12 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
}
}
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?.groupChatsMode, .allMessages)
@ -119,9 +126,12 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
}
}
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)
@ -141,23 +151,22 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
}
}
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: .oneToOneChat, isEncrypted: false)])
let deferredState = deferFulfillment(viewModel.context.$viewState
.map(\.fixingConfigurationMismatch)
.removeDuplicates()
.collect(3)
.first())
context.send(viewAction: .fixConfigurationMismatchTapped)
let fixingStates = try await deferredState.fulfill()
deferred = deferFulfillment(viewModel.context.$viewState, keyPath: \.fixingConfigurationMismatch, transitionValues: [false, true, false])
XCTAssertEqual(fixingStates, [false, true, false])
context.send(viewAction: .fixConfigurationMismatchTapped)
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)
@ -180,23 +189,30 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
}
}
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)])
let deferredState = deferFulfillment(viewModel.context.$viewState
.map(\.fixingConfigurationMismatch)
.removeDuplicates()
.collect(3)
.first())
context.send(viewAction: .fixConfigurationMismatchTapped)
let fixingStates = try await deferredState.fulfill()
deferred = deferFulfillment(viewModel.context.$viewState) { state in
state.fixingConfigurationMismatch == true
}
XCTAssertEqual(fixingStates, [false, true, false])
context.send(viewAction: .fixConfigurationMismatchTapped)
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)
@ -213,15 +229,23 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
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 }))
let deferred = deferFulfillment(notificationSettingsProxy.callbacks) { callback in
callback == .settingsDidChange
}
context.send(viewAction: .roomMentionChanged)
try await deferred.fulfill()
XCTAssert(notificationSettingsProxy.setRoomMentionEnabledEnabledCalled)
@ -230,15 +254,22 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
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 }))
let deferred = deferFulfillment(notificationSettingsProxy.callbacks) { callback in
callback == .settingsDidChange
}
context.send(viewAction: .roomMentionChanged)
try await deferred.fulfill()
XCTAssert(notificationSettingsProxy.setRoomMentionEnabledEnabledCalled)
@ -248,34 +279,52 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
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())
context.send(viewAction: .roomMentionChanged)
let states = try await deferred.fulfill()
XCTAssertEqual(states, [false, true, false])
var deferred = deferFulfillment(context.$viewState) { state in
state.applyingChange == true
}
context.send(viewAction: .roomMentionChanged)
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.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 = false
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)
@ -284,15 +333,23 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
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)
@ -302,20 +359,31 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
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)
}
}

View File

@ -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.")

View File

@ -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)
}

View File

@ -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())
context.send(viewAction: .processTapLeave)
let states = try await deferred.fulfill()
let deferred = deferFulfillment(context.$viewState) { state in
state.bindings.leaveRoomAlertItem != nil
}
XCTAssertNil(states[0].bindings.leaveRoomAlertItem)
XCTAssertEqual(states[1].bindings.leaveRoomAlertItem?.state, .public)
XCTAssertEqual(states[1].bindings.leaveRoomAlertItem?.subtitle, L10n.leaveRoomAlertSubtitle)
context.send(viewAction: .processTapLeave)
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)
}
@ -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,7 +444,11 @@ 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()
@ -404,7 +457,11 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
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()
@ -413,7 +470,11 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
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()
@ -425,7 +486,11 @@ 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()
@ -497,7 +562,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()
}
@ -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()
}

View File

@ -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<RoomFlowCoordinatorAction>]()
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()
}
}
}

View File

@ -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)
}
@ -81,16 +82,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.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)
}
@ -108,15 +110,16 @@ 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)
}
@ -135,16 +138,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.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)
}

View File

@ -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)

View File

@ -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<AnyCancellable>()
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(context.$viewState.map(\.isRestoringDefaultSetting)
.removeDuplicates()
.collect(3).first())
let deferredIsRestoringDefaultSettings = deferFulfillment(viewModel.context.$viewState,
keyPath: \.isRestoringDefaultSetting,
transitionValues: [false, true, false])
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])
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])
// 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)
}

View File

@ -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)

View File

@ -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)

View File

@ -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'")

View File

@ -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',
)