mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
b70e77f7c5
An unapproved account only gets the server's default contacts, which are its way to reach support — list them instead of discarding them, and hide the search field that cannot return anything.
260 lines
11 KiB
Swift
260 lines
11 KiB
Swift
//
|
|
// Copyright 2026 Element Creations Ltd.
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
// Please see LICENSE files in the repository root for full details.
|
|
//
|
|
|
|
@testable import ElementX
|
|
import Foundation
|
|
import Testing
|
|
|
|
final class HealthChatMemberDirectoryScreenViewModelTests {
|
|
private let apiService = HealthChatAPIServiceMock()
|
|
private let clientProxy = ClientProxyMock(.init())
|
|
|
|
init() throws {
|
|
apiService.profileCompletenessReturnValue = try .success(makeCompleteness(isComplete: true, message: nil))
|
|
}
|
|
|
|
@Test
|
|
func loadsMembersOnStart() async throws {
|
|
apiService.membersSearchQueryReturnValue = try .success(makeResponse(userIDs: ["@alice:healthchat.ch"]))
|
|
|
|
let viewModel = makeViewModel()
|
|
|
|
let deferred = deferFulfillment(viewModel.context.observe(\.viewState.content)) { content in
|
|
guard case .members(let members) = content else { return false }
|
|
return members.map(\.userID) == ["@alice:healthchat.ch"]
|
|
}
|
|
try await deferred.fulfill()
|
|
}
|
|
|
|
@Test
|
|
func showsPendingApprovalMessageWithTheDefaultContacts() async throws {
|
|
apiService.membersSearchQueryReturnValue = try .success(makeResponse(userIDs: ["@natascha.katumba:healthchat.ch"],
|
|
isApproved: false,
|
|
pendingMessage: "Ihr Konto wird geprüft"))
|
|
|
|
let viewModel = makeViewModel()
|
|
|
|
let deferred = deferFulfillment(viewModel.context.observe(\.viewState.content)) { content in
|
|
guard case .pendingApproval(let message, let members) = content else { return false }
|
|
return message == "Ihr Konto wird geprüft" && members.map(\.userID) == ["@natascha.katumba:healthchat.ch"]
|
|
}
|
|
try await deferred.fulfill()
|
|
}
|
|
|
|
@Test
|
|
func refreshPicksUpAnApprovalWithoutClearingTheList() async throws {
|
|
apiService.membersSearchQueryReturnValue = try .success(makeResponse(userIDs: ["@natascha.katumba:healthchat.ch"],
|
|
isApproved: false,
|
|
pendingMessage: "Ihr Konto wird geprüft"))
|
|
|
|
let viewModel = makeViewModel()
|
|
|
|
let deferredPending = deferFulfillment(viewModel.context.observe(\.viewState.content)) { content in
|
|
guard case .pendingApproval = content else { return false }
|
|
return true
|
|
}
|
|
try await deferredPending.fulfill()
|
|
|
|
apiService.membersSearchQueryReturnValue = try .success(makeResponse(userIDs: ["@alice:healthchat.ch"]))
|
|
|
|
let deferredMembers = deferFulfillment(viewModel.context.observe(\.viewState.content)) { content in
|
|
guard case .members(let members) = content else { return false }
|
|
return members.map(\.userID) == ["@alice:healthchat.ch"]
|
|
}
|
|
viewModel.context.send(viewAction: .refresh)
|
|
try await deferredMembers.fulfill()
|
|
|
|
#expect(viewModel.context.viewState.isApproved)
|
|
}
|
|
|
|
@Test
|
|
func showsFailureAndRecoversOnRetry() async throws {
|
|
apiService.membersSearchQueryReturnValue = .failure(.networkFailure)
|
|
|
|
let viewModel = makeViewModel()
|
|
|
|
let deferredFailure = deferFulfillment(viewModel.context.observe(\.viewState.content)) { $0 == .failure }
|
|
try await deferredFailure.fulfill()
|
|
|
|
apiService.membersSearchQueryReturnValue = try .success(makeResponse(userIDs: ["@alice:healthchat.ch"]))
|
|
|
|
let deferredMembers = deferFulfillment(viewModel.context.observe(\.viewState.content)) { content in
|
|
guard case .members = content else { return false }
|
|
return true
|
|
}
|
|
viewModel.context.send(viewAction: .retry)
|
|
try await deferredMembers.fulfill()
|
|
}
|
|
|
|
@Test
|
|
func searchQueryTriggersServerSearch() async throws {
|
|
let allMembers = try makeResponse(userIDs: ["@alice:healthchat.ch"])
|
|
let searchResults = try makeResponse(userIDs: ["@zoe:healthchat.ch"])
|
|
apiService.membersSearchQueryClosure = { searchQuery in
|
|
searchQuery == "Zürich" ? .success(searchResults) : .success(allMembers)
|
|
}
|
|
|
|
let viewModel = makeViewModel()
|
|
|
|
let deferred = deferFulfillment(viewModel.context.observe(\.viewState.content)) { content in
|
|
guard case .members(let members) = content else { return false }
|
|
return members.map(\.userID) == ["@zoe:healthchat.ch"]
|
|
}
|
|
viewModel.context.searchQuery = "Zürich"
|
|
try await deferred.fulfill()
|
|
|
|
#expect(apiService.membersSearchQueryReceivedInvocations.last == "Zürich")
|
|
}
|
|
|
|
@Test
|
|
func startChatWithExistingDMPresentsRoom() async throws {
|
|
apiService.membersSearchQueryReturnValue = try .success(makeResponse(userIDs: ["@alice:healthchat.ch"]))
|
|
clientProxy.directRoomForUserIDReturnValue = .success("existingRoom")
|
|
|
|
let viewModel = makeViewModel()
|
|
|
|
let deferred = deferFulfillment(viewModel.actionsPublisher) { action in
|
|
guard case .presentRoom(let roomID) = action else { return false }
|
|
return roomID == "existingRoom"
|
|
}
|
|
try viewModel.context.send(viewAction: .startChat(makeMember(userID: "@alice:healthchat.ch")))
|
|
try await deferred.fulfill()
|
|
}
|
|
|
|
@Test
|
|
func startChatWithoutDMCreatesOne() async throws {
|
|
apiService.membersSearchQueryReturnValue = try .success(makeResponse(userIDs: ["@alice:healthchat.ch"]))
|
|
clientProxy.directRoomForUserIDReturnValue = .success(nil)
|
|
clientProxy.createDirectRoomWithExpectedRoomNameReturnValue = .success("newRoom")
|
|
|
|
let viewModel = makeViewModel()
|
|
|
|
let deferred = deferFulfillment(viewModel.actionsPublisher) { action in
|
|
guard case .presentRoom(let roomID) = action else { return false }
|
|
return roomID == "newRoom"
|
|
}
|
|
try viewModel.context.send(viewAction: .startChat(makeMember(userID: "@alice:healthchat.ch")))
|
|
try await deferred.fulfill()
|
|
|
|
#expect(clientProxy.createDirectRoomWithExpectedRoomNameReceivedArguments?.userID == "@alice:healthchat.ch")
|
|
}
|
|
|
|
@Test
|
|
func selectMemberEmitsAction() async throws {
|
|
apiService.membersSearchQueryReturnValue = try .success(makeResponse(userIDs: ["@alice:healthchat.ch"]))
|
|
|
|
let viewModel = makeViewModel()
|
|
|
|
let deferred = deferFulfillment(viewModel.actionsPublisher) { action in
|
|
guard case .selectMember(let userID) = action else { return false }
|
|
return userID == "@alice:healthchat.ch"
|
|
}
|
|
try viewModel.context.send(viewAction: .selectMember(makeMember(userID: "@alice:healthchat.ch")))
|
|
try await deferred.fulfill()
|
|
}
|
|
|
|
@Test
|
|
func showsCompletenessHintWhenProfileIsIncomplete() async throws {
|
|
apiService.membersSearchQueryReturnValue = try .success(makeResponse(userIDs: ["@alice:healthchat.ch"]))
|
|
apiService.profileCompletenessReturnValue = try .success(makeCompleteness(isComplete: false,
|
|
message: "Profil unvollständig"))
|
|
|
|
let viewModel = makeViewModel()
|
|
|
|
let deferred = deferFulfillment(viewModel.context.observe(\.viewState.completenessMessage)) { $0 == "Profil unvollständig" }
|
|
try await deferred.fulfill()
|
|
|
|
#expect(viewModel.context.viewState.isApproved)
|
|
}
|
|
|
|
@Test
|
|
func hidesCompletenessHintWhenProfileIsComplete() async throws {
|
|
apiService.membersSearchQueryReturnValue = try .success(makeResponse(userIDs: ["@alice:healthchat.ch"]))
|
|
|
|
let viewModel = makeViewModel()
|
|
|
|
let deferred = deferFulfillment(viewModel.context.observe(\.viewState.content)) { content in
|
|
guard case .members = content else { return false }
|
|
return true
|
|
}
|
|
try await deferred.fulfill()
|
|
|
|
#expect(viewModel.context.viewState.completenessMessage == nil)
|
|
}
|
|
|
|
@Test
|
|
func pendingApprovalDisablesSearchingAndInviting() async throws {
|
|
apiService.membersSearchQueryReturnValue = try .success(makeResponse(userIDs: [],
|
|
isApproved: false,
|
|
pendingMessage: "Ihr Konto wird geprüft"))
|
|
|
|
let viewModel = makeViewModel()
|
|
|
|
let deferred = deferFulfillment(viewModel.context.observe(\.viewState.content)) { content in
|
|
guard case .pendingApproval = content else { return false }
|
|
return true
|
|
}
|
|
try await deferred.fulfill()
|
|
|
|
#expect(!viewModel.context.viewState.isApproved)
|
|
}
|
|
|
|
@Test
|
|
func editProfileEmitsAction() async throws {
|
|
apiService.membersSearchQueryReturnValue = try .success(makeResponse(userIDs: []))
|
|
|
|
let viewModel = makeViewModel()
|
|
|
|
let deferred = deferFulfillment(viewModel.actionsPublisher) { action in
|
|
guard case .editProfile = action else { return false }
|
|
return true
|
|
}
|
|
viewModel.context.send(viewAction: .editProfile)
|
|
try await deferred.fulfill()
|
|
}
|
|
|
|
@Test
|
|
func inviteEmitsAction() async throws {
|
|
apiService.membersSearchQueryReturnValue = try .success(makeResponse(userIDs: []))
|
|
|
|
let viewModel = makeViewModel()
|
|
|
|
let deferred = deferFulfillment(viewModel.actionsPublisher) { action in
|
|
guard case .invite = action else { return false }
|
|
return true
|
|
}
|
|
viewModel.context.send(viewAction: .invite)
|
|
try await deferred.fulfill()
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private func makeCompleteness(isComplete: Bool, message: String?) throws -> HealthChatProfileCompleteness {
|
|
let messageJSON = message.map { "\"\($0)\"" } ?? "null"
|
|
return try JSONDecoder().decode(HealthChatProfileCompleteness.self, from: Data("""
|
|
{ "isComplete": \(isComplete), "message": \(messageJSON) }
|
|
""".utf8))
|
|
}
|
|
|
|
private func makeViewModel() -> HealthChatMemberDirectoryScreenViewModel {
|
|
HealthChatMemberDirectoryScreenViewModel(apiService: apiService,
|
|
clientProxy: clientProxy,
|
|
mediaProvider: MediaProviderMock(.init()),
|
|
userIndicatorController: UserIndicatorControllerMock())
|
|
}
|
|
|
|
private func makeResponse(userIDs: [String],
|
|
isApproved: Bool = true,
|
|
pendingMessage: String? = nil) throws -> HealthChatMembersResponse {
|
|
try .init(users: userIDs.map(makeMember(userID:)), isApproved: isApproved, pendingMessage: pendingMessage)
|
|
}
|
|
|
|
private func makeMember(userID: String) throws -> HealthChatMember {
|
|
try JSONDecoder().decode(HealthChatMember.self, from: Data(#"{ "userId": "\#(userID)" }"#.utf8))
|
|
}
|
|
}
|