// // 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()) @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 showsPendingApprovalMessage() async throws { apiService.membersSearchQueryReturnValue = .success(.init(users: [], isApproved: false, pendingMessage: "Ihr Konto wird geprüft")) let viewModel = makeViewModel() let deferred = deferFulfillment(viewModel.context.observe(\.viewState.content)) { content in content == .pendingApproval(message: "Ihr Konto wird geprüft") } try await deferred.fulfill() } @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() } // MARK: - Helpers private func makeViewModel() -> HealthChatMemberDirectoryScreenViewModel { HealthChatMemberDirectoryScreenViewModel(apiService: apiService, clientProxy: clientProxy, mediaProvider: MediaProviderMock(.init()), userIndicatorController: UserIndicatorControllerMock()) } private func makeResponse(userIDs: [String]) throws -> HealthChatMembersResponse { try .init(users: userIDs.map(makeMember(userID:)), isApproved: true, pendingMessage: nil) } private func makeMember(userID: String) throws -> HealthChatMember { try JSONDecoder().decode(HealthChatMember.self, from: Data(#"{ "userId": "\#(userID)" }"#.utf8)) } }