// // 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 Testing @MainActor struct StartChatScreenViewModelTests { private var viewModel: StartChatScreenViewModelProtocol! private var clientProxy: ClientProxyMock! private var userDiscoveryService: UserDiscoveryServiceMock! private var context: StartChatScreenViewModel.Context { viewModel.context } init() { clientProxy = .init(.init(userID: "")) userDiscoveryService = UserDiscoveryServiceMock() userDiscoveryService.searchProfilesWithReturnValue = .success([]) let userSession = UserSessionMock(.init(clientProxy: clientProxy)) viewModel = StartChatScreenViewModel(userSession: userSession, analytics: ServiceLocator.shared.analytics, userIndicatorController: UserIndicatorControllerMock(), userDiscoveryService: userDiscoveryService, appSettings: ServiceLocator.shared.settings) } @Test mutating func queryShowingNoResults() async { await search(query: "A") #expect(context.viewState.usersSection.type == .suggestions) await search(query: "AA") #expect(context.viewState.usersSection.type == .suggestions) #expect(!userDiscoveryService.searchProfilesWithCalled) await search(query: "AAA") assertSearchResults(toBe: 0) #expect(userDiscoveryService.searchProfilesWithCalled) } @Test func joinRoomByAddress() async throws { clientProxy.resolveRoomAliasReturnValue = .success(.init(roomId: "id", servers: [])) let deferredViewState = deferFulfillment(viewModel.context.$viewState) { viewState in viewState.joinByAddressState == .addressFound(address: "#room:example.com", roomID: "id") } viewModel.context.roomAddress = "#room:example.com" try await deferredViewState.fulfill() let deferredAction = deferFulfillment(viewModel.actions) { action in action == .showRoom(roomID: "id") } context.send(viewAction: .joinRoomByAddress) try await deferredAction.fulfill() } @Test func joinRoomByAddressFailsBecauseInvalid() async throws { let deferred = deferFulfillment(viewModel.context.$viewState) { viewState in viewState.joinByAddressState == .invalidAddress } viewModel.context.roomAddress = ":" context.send(viewAction: .joinRoomByAddress) try await deferred.fulfill() } @Test func joinRoomByAddressFailsBecauseNotFound() async throws { clientProxy.resolveRoomAliasReturnValue = .failure(.failedResolvingRoomAlias) let deferred = deferFulfillment(viewModel.context.$viewState) { viewState in viewState.joinByAddressState == .addressNotFound } viewModel.context.roomAddress = "#room:example.com" context.send(viewAction: .joinRoomByAddress) try await deferred.fulfill() } // MARK: - History Sharing @Test func inviteConfirmationFetchesIdentity() async throws { clientProxy.directRoomForUserIDReturnValue = .success(nil) clientProxy.userIdentityForFallBackToServerReturnValue = .success(UserIdentityProxyMock(configuration: .init(verificationState: .notVerified))) // User identity becomes known, i.e. not unknown let deferred = deferFulfillment(viewModel.context.$viewState.compactMap(\.bindings.selectedUserToInvite)) { !$0.isUnknown } context.send(viewAction: .selectUser(.mockBob)) try await deferred.fulfill() #expect(clientProxy.userIdentityForFallBackToServerCalled) } @Test func inviteConfirmationFallsBackToUnknownIdentityOnFailure() async throws { clientProxy.directRoomForUserIDReturnValue = .success(nil) clientProxy.userIdentityForFallBackToServerReturnValue = .failure(.forbiddenAccess) // User identity never becomes known, i.e. is never not unknown let deferred = deferFailure(viewModel.context.$viewState.compactMap(\.bindings.selectedUserToInvite), timeout: .seconds(5)) { !$0.isUnknown } context.send(viewAction: .selectUser(.mockBob)) try await deferred.fulfill() #expect(clientProxy.userIdentityForFallBackToServerCalled) } // MARK: - Private private func assertSearchResults(toBe count: Int) { #expect(count >= 0) #expect(context.viewState.usersSection.type == .searchResult) #expect(context.viewState.usersSection.users.count == count) #expect(context.viewState.hasEmptySearchResults == (count == 0)) } @discardableResult private mutating func search(query: String) async -> StartChatScreenViewState? { viewModel.context.searchQuery = query return await context.$viewState.nextValue } }