Files
element-x-ios/UnitTests/Sources/NotificationSettingsEditScreenViewModelTests.swift
T
Mauro 2d486ece33 Use iOS 26 Observation for Unit and Preview tests. (#5797)
* use observation in unit and preview tests instead of the async stream

* use a parsing approach that is independent of the sourcery annotation

* Fix observation deferred fulfillments not receiving the initial value

Observation's observe only emits transitions, not the value present at subscription time. Drop the leading initial value from the affected transitionValues sequences and assert it directly with #expect instead.

* improved docs
2026-06-30 14:01:08 +02:00

266 lines
12 KiB
Swift

//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
@testable import ElementX
import MatrixRustSDK
import Testing
@MainActor
struct NotificationSettingsEditScreenViewModelTests {
private var viewModel: NotificationSettingsEditScreenViewModelProtocol!
private var notificationSettingsProxy: NotificationSettingsProxyMock!
private var userSession: UserSessionMock!
private var clientProxy: ClientProxyMock!
private var context: NotificationSettingsEditScreenViewModelType.Context {
viewModel.context
}
init() throws {
notificationSettingsProxy = NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration())
notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .allMessages
clientProxy = ClientProxyMock(.init(userID: "@a:b.com", notificationSettings: notificationSettingsProxy))
userSession = UserSessionMock(.init(clientProxy: clientProxy))
}
@Test
mutating func fetchSettings() async throws {
notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in
switch (isEncrypted, isOneToOne) {
case (_, true):
return .allMessages
case (_, _):
return .mentionsAndKeywordsOnly
}
}
viewModel = NotificationSettingsEditScreenViewModel(chatType: .groupChat, userSession: userSession)
let deferred = deferFulfillment(viewModel.context.observe(\.viewState.defaultMode)) { $0 != nil }
viewModel.fetchInitialContent()
try await deferred.fulfill()
// `getDefaultRoomNotificationModeIsEncryptedIsOneToOne` must have been called twice (for encrypted and unencrypted group chats)
let invocations = notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReceivedInvocations
#expect(invocations.count == 2)
// First call for encrypted group chats
#expect(invocations[0].isEncrypted == true)
#expect(invocations[0].isOneToOne == false)
// Second call for unencrypted group chats
#expect(invocations[1].isEncrypted == false)
#expect(invocations[1].isOneToOne == false)
#expect(context.viewState.defaultMode == .mentionsAndKeywordsOnly)
#expect(context.viewState.bindings.alertInfo == nil)
#expect(!context.viewState.canPushEncryptedEvents)
#expect(context.viewState.description(for: .mentionsAndKeywordsOnly) != nil)
}
@Test
mutating func fetchSettingsWithCanPushEncryptedEvents() async throws {
notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in
switch (isEncrypted, isOneToOne) {
case (_, true):
return .allMessages
case (_, _):
return .mentionsAndKeywordsOnly
}
}
notificationSettingsProxy.canPushEncryptedEventsToDeviceClosure = {
true
}
viewModel = NotificationSettingsEditScreenViewModel(chatType: .groupChat, userSession: userSession)
let deferred = deferFulfillment(viewModel.context.observe(\.viewState.defaultMode)) { $0 != nil }
viewModel.fetchInitialContent()
try await deferred.fulfill()
// `getDefaultRoomNotificationModeIsEncryptedIsOneToOne` must have been called twice (for encrypted and unencrypted group chats)
let invocations = notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReceivedInvocations
#expect(invocations.count == 2)
// First call for encrypted group chats
#expect(invocations[0].isEncrypted == true)
#expect(invocations[0].isOneToOne == false)
// Second call for unencrypted group chats
#expect(invocations[1].isEncrypted == false)
#expect(invocations[1].isOneToOne == false)
#expect(context.viewState.defaultMode == .mentionsAndKeywordsOnly)
#expect(context.viewState.bindings.alertInfo == nil)
#expect(context.viewState.canPushEncryptedEvents)
#expect(context.viewState.description(for: .mentionsAndKeywordsOnly) == nil)
}
@Test
mutating func setModeAllMessages() async throws {
notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .mentionsAndKeywordsOnly
viewModel = NotificationSettingsEditScreenViewModel(chatType: .groupChat, userSession: userSession)
let deferred = deferFulfillment(viewModel.context.observe(\.viewState.defaultMode)) { $0 != nil }
viewModel.fetchInitialContent()
try await deferred.fulfill()
#expect(viewModel.context.viewState.pendingMode == nil)
var deferredViewState = deferFulfillment(viewModel.context.observe(\.viewState.pendingMode),
transitionValues: [.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
#expect(notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount == 2)
// First call for encrypted group chats
#expect(invocations[0].isEncrypted == true)
#expect(invocations[0].isOneToOne == false)
#expect(invocations[0].mode == .allMessages)
// Second call for unencrypted group chats
#expect(invocations[1].isEncrypted == false)
#expect(invocations[1].isOneToOne == false)
#expect(invocations[1].mode == .allMessages)
deferredViewState = deferFulfillment(viewModel.context.observe(\.viewState.defaultMode),
transitionValues: [.allMessages])
try await deferredViewState.fulfill()
#expect(context.viewState.defaultMode == .allMessages)
#expect(context.viewState.bindings.alertInfo == nil)
}
@Test
mutating func setModeMentions() async throws {
viewModel = NotificationSettingsEditScreenViewModel(chatType: .groupChat, userSession: userSession)
let deferred = deferFulfillment(viewModel.context.observe(\.viewState.defaultMode)) { $0 != nil }
viewModel.fetchInitialContent()
try await deferred.fulfill()
#expect(viewModel.context.viewState.pendingMode == nil)
var deferredViewState = deferFulfillment(viewModel.context.observe(\.viewState.pendingMode),
transitionValues: [.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
#expect(notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount == 2)
// First call for encrypted group chats
#expect(invocations[0].isEncrypted == true)
#expect(invocations[0].isOneToOne == false)
#expect(invocations[0].mode == .mentionsAndKeywordsOnly)
// Second call for unencrypted group chats
#expect(invocations[1].isEncrypted == false)
#expect(invocations[1].isOneToOne == false)
#expect(invocations[1].mode == .mentionsAndKeywordsOnly)
deferredViewState = deferFulfillment(viewModel.context.observe(\.viewState.defaultMode),
transitionValues: [.mentionsAndKeywordsOnly])
try await deferredViewState.fulfill()
#expect(context.viewState.defaultMode == .mentionsAndKeywordsOnly)
#expect(context.viewState.bindings.alertInfo == nil)
}
@Test
mutating func setModeDirectChats() async throws {
notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .mentionsAndKeywordsOnly
// Initialize for direct chats
viewModel = NotificationSettingsEditScreenViewModel(chatType: .oneToOneChat, userSession: userSession)
let deferred = deferFulfillment(viewModel.context.observe(\.viewState.defaultMode)) { $0 != nil }
viewModel.fetchInitialContent()
try await deferred.fulfill()
#expect(viewModel.context.viewState.pendingMode == nil)
let deferredViewState = deferFulfillment(viewModel.context.observe(\.viewState.pendingMode),
transitionValues: [.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
#expect(notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount == 2)
// First call for encrypted direct chats
#expect(invocations[0].isEncrypted == true)
#expect(invocations[0].isOneToOne == true)
#expect(invocations[0].mode == .allMessages)
// Second call for unencrypted direct chats
#expect(invocations[1].isEncrypted == false)
#expect(invocations[1].isOneToOne == true)
#expect(invocations[1].mode == .allMessages)
}
@Test
mutating func setModeFailure() async throws {
notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .mentionsAndKeywordsOnly
notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeThrowableError = NotificationSettingsError.Generic(msg: "error")
viewModel = NotificationSettingsEditScreenViewModel(chatType: .oneToOneChat, userSession: userSession)
let deferred = deferFulfillment(viewModel.context.observe(\.viewState.defaultMode)) { $0 != nil }
viewModel.fetchInitialContent()
try await deferred.fulfill()
#expect(viewModel.context.viewState.pendingMode == nil)
let deferredViewState = deferFulfillment(viewModel.context.observe(\.viewState.pendingMode),
transitionValues: [.allMessages, nil])
context.send(viewAction: .setMode(.allMessages))
try await deferredViewState.fulfill()
#expect(context.viewState.bindings.alertInfo != nil)
}
@Test
mutating func selectRoom() async throws {
let roomID = "!roomidentifier:matrix.org"
viewModel = NotificationSettingsEditScreenViewModel(chatType: .oneToOneChat, userSession: userSession)
let deferredActions = deferFulfillment(viewModel.actions) { action in
switch action {
case .requestRoomNotificationSettingsPresentation:
return true
}
}
context.send(viewAction: .selectRoom(roomIdentifier: roomID))
let sentAction = try await deferredActions.fulfill()
let expectedAction = NotificationSettingsEditScreenViewModelAction.requestRoomNotificationSettingsPresentation(roomID: roomID)
guard case let .requestRoomNotificationSettingsPresentation(roomID: receivedRoomID) = sentAction, receivedRoomID == roomID else {
Issue.record("Expected action \(expectedAction), but was \(sentAction)")
return
}
}
}