* Present members of a space * present the members modally from the space * Implemented a room members flow coordinator to make such flow more modular and reusable this is required since we will need to reuse this module also in the space settings, and later we could also replace it in the RoomFlowCoordinator. * the implementation to support at least the SpaceFlowCoordinator is done a follow UP should do the refactor. * remove modal usage from the flow, we want to always be a navigation flow * Improved and implemented the room navigation in the members flow coordinator * pr suggestions and refactored the start chat flow and the invite screen * updated copies for managing room members * Update ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --------- Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>
203 lines
9.2 KiB
Swift
203 lines
9.2 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.
|
|
//
|
|
|
|
import Combine
|
|
import XCTest
|
|
|
|
@testable import ElementX
|
|
|
|
@MainActor
|
|
class CreateRoomScreenViewModelTests: XCTestCase {
|
|
var viewModel: CreateRoomViewModelProtocol!
|
|
var clientProxy: ClientProxyMock!
|
|
var userSession: UserSessionMock!
|
|
|
|
private let usersSubject = CurrentValueSubject<[UserProfileProxy], Never>([])
|
|
|
|
var context: CreateRoomViewModel.Context {
|
|
viewModel.context
|
|
}
|
|
|
|
override func setUpWithError() throws {
|
|
clientProxy = ClientProxyMock(.init(userIDServerName: "matrix.org", userID: "@a:b.com"))
|
|
userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
|
let parameters = CreateRoomFlowParameters()
|
|
ServiceLocator.shared.settings.knockingEnabled = true
|
|
let viewModel = CreateRoomViewModel(userSession: userSession,
|
|
createRoomParameters: .init(parameters),
|
|
selectedUsers: [.mockAlice, .mockBob, .mockCharlie],
|
|
analytics: ServiceLocator.shared.analytics,
|
|
userIndicatorController: UserIndicatorControllerMock(),
|
|
appSettings: ServiceLocator.shared.settings)
|
|
self.viewModel = viewModel
|
|
}
|
|
|
|
func testDeselectUser() {
|
|
XCTAssertFalse(context.viewState.selectedUsers.isEmpty)
|
|
XCTAssertEqual(context.viewState.selectedUsers.count, 3)
|
|
XCTAssertEqual(context.viewState.selectedUsers.first?.userID, UserProfileProxy.mockAlice.userID)
|
|
context.send(viewAction: .deselectUser(.mockAlice))
|
|
XCTAssertNotEqual(context.viewState.selectedUsers.first?.userID, UserProfileProxy.mockAlice.userID)
|
|
}
|
|
|
|
func testDefaultSecurity() {
|
|
XCTAssertTrue(context.viewState.bindings.isRoomPrivate)
|
|
}
|
|
|
|
func testCreateRoomRequirements() {
|
|
XCTAssertFalse(context.viewState.canCreateRoom)
|
|
context.send(viewAction: .updateRoomName("A"))
|
|
XCTAssertTrue(context.viewState.canCreateRoom)
|
|
}
|
|
|
|
func testCreateRoom() async throws {
|
|
// Given a form with a blank topic.
|
|
context.send(viewAction: .updateRoomName("A"))
|
|
context.roomTopic = ""
|
|
context.isRoomPrivate = false
|
|
XCTAssertTrue(context.viewState.canCreateRoom)
|
|
|
|
// When creating the room.
|
|
clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartReturnValue = .success("1")
|
|
let deferred = deferFulfillment(viewModel.actions) { action in
|
|
guard case .openRoom("1") = action else { return false }
|
|
return true
|
|
}
|
|
context.send(viewAction: .createRoom)
|
|
try await deferred.fulfill()
|
|
|
|
// Then the room should be created and a topic should not be set.
|
|
XCTAssertTrue(clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartCalled)
|
|
XCTAssertEqual(clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartReceivedArguments?.name, "A")
|
|
XCTAssertNil(clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartReceivedArguments?.topic,
|
|
"The topic should be sent as nil when it is empty.")
|
|
}
|
|
|
|
func testCreateKnockingRoom() async {
|
|
context.send(viewAction: .updateRoomName("A"))
|
|
context.roomTopic = "B"
|
|
context.isRoomPrivate = false
|
|
// When setting the room as private we always reset the knocking state to the default value of false
|
|
// so we need to wait a main actor cycle to ensure the view state is updated
|
|
await Task.yield()
|
|
context.isKnockingOnly = true
|
|
XCTAssertTrue(context.viewState.canCreateRoom)
|
|
|
|
let expectation = expectation(description: "Wait for the room to be created")
|
|
clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartClosure = { _, _, isPrivate, isKnockingOnly, _, _, localAliasPart in
|
|
XCTAssertTrue(isKnockingOnly)
|
|
XCTAssertFalse(isPrivate)
|
|
XCTAssertEqual(localAliasPart, "a")
|
|
defer { expectation.fulfill() }
|
|
return .success("")
|
|
}
|
|
context.send(viewAction: .createRoom)
|
|
await fulfillment(of: [expectation])
|
|
}
|
|
|
|
func testCreatePublicRoomFailsForInvalidAlias() async throws {
|
|
context.send(viewAction: .updateRoomName("A"))
|
|
context.roomTopic = "B"
|
|
context.isRoomPrivate = false
|
|
// When setting the room as private we always reset the alias
|
|
// so we need to wait a main actor cycle to ensure the view state is updated
|
|
await Task.yield()
|
|
|
|
// we wait for the debounce to show the error
|
|
let deferred = deferFulfillment(context.$viewState) { viewState in
|
|
viewState.aliasErrors.contains(.invalidSymbols) && !viewState.canCreateRoom
|
|
}
|
|
context.send(viewAction: .updateAliasLocalPart("#:"))
|
|
try await deferred.fulfill()
|
|
|
|
// We also want to force the room creation in case the user may tap the button before the debounce
|
|
// blocked it
|
|
context.send(viewAction: .createRoom)
|
|
await Task.yield()
|
|
XCTAssertFalse(clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartCalled)
|
|
}
|
|
|
|
func testCreatePublicRoomFailsForExistingAlias() async throws {
|
|
clientProxy.isAliasAvailableReturnValue = .success(false)
|
|
context.send(viewAction: .updateRoomName("A"))
|
|
context.roomTopic = "B"
|
|
context.isRoomPrivate = false
|
|
// When setting the room as private we always reset the alias
|
|
// so we need to wait a main actor cycle to ensure the view state is updated
|
|
await Task.yield()
|
|
|
|
// we wait for the debounce to show the error
|
|
let deferred = deferFulfillment(context.$viewState) { viewState in
|
|
viewState.aliasErrors.contains(.alreadyExists) && !viewState.canCreateRoom
|
|
}
|
|
context.send(viewAction: .updateAliasLocalPart("abc"))
|
|
try await deferred.fulfill()
|
|
|
|
// We also want to force the room creation in case the user may tap the button before the debounce
|
|
// blocked it
|
|
let expectation = expectation(description: "Wait for the alias to be checked again")
|
|
clientProxy.isAliasAvailableClosure = { _ in
|
|
defer {
|
|
expectation.fulfill()
|
|
}
|
|
return .success(false)
|
|
}
|
|
context.send(viewAction: .createRoom)
|
|
await fulfillment(of: [expectation])
|
|
XCTAssertEqual(clientProxy.isAliasAvailableCallsCount, 2)
|
|
XCTAssertFalse(clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartCalled)
|
|
}
|
|
|
|
func testCreatePrivateRoomCantHaveKnockRule() async {
|
|
context.send(viewAction: .updateRoomName("A"))
|
|
context.roomTopic = "B"
|
|
context.isRoomPrivate = true
|
|
context.isKnockingOnly = true
|
|
context.send(viewAction: .createRoom)
|
|
let expectation = expectation(description: "Wait for the room to be created")
|
|
clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartClosure = { _, _, isPrivate, isKnockingOnly, _, _, _ in
|
|
XCTAssertFalse(isKnockingOnly)
|
|
XCTAssertTrue(isPrivate)
|
|
expectation.fulfill()
|
|
return .success("")
|
|
}
|
|
await fulfillment(of: [expectation])
|
|
}
|
|
|
|
func testNameAndAddressSync() async {
|
|
context.isRoomPrivate = true
|
|
await Task.yield()
|
|
context.send(viewAction: .updateRoomName("abc"))
|
|
XCTAssertEqual(context.viewState.aliasLocalPart, "abc")
|
|
XCTAssertEqual(context.viewState.roomName, "abc")
|
|
context.send(viewAction: .updateRoomName("DEF"))
|
|
XCTAssertEqual(context.viewState.roomName, "DEF")
|
|
XCTAssertEqual(context.viewState.aliasLocalPart, "def")
|
|
context.send(viewAction: .updateRoomName("a b c"))
|
|
XCTAssertEqual(context.viewState.aliasLocalPart, "a-b-c")
|
|
XCTAssertEqual(context.viewState.roomName, "a b c")
|
|
context.send(viewAction: .updateAliasLocalPart("hello-world"))
|
|
// This removes the sync
|
|
XCTAssertEqual(context.viewState.aliasLocalPart, "hello-world")
|
|
XCTAssertEqual(context.viewState.roomName, "a b c")
|
|
|
|
context.send(viewAction: .updateRoomName("Hello Matrix!"))
|
|
XCTAssertEqual(context.viewState.aliasLocalPart, "hello-world")
|
|
XCTAssertEqual(context.viewState.roomName, "Hello Matrix!")
|
|
|
|
// Deleting the whole name will restore the sync
|
|
context.send(viewAction: .updateRoomName(""))
|
|
XCTAssertEqual(context.viewState.aliasLocalPart, "")
|
|
XCTAssertEqual(context.viewState.roomName, "")
|
|
|
|
context.send(viewAction: .updateRoomName("Hello# Matrix!"))
|
|
XCTAssertEqual(context.viewState.aliasLocalPart, "hello-matrix!")
|
|
XCTAssertEqual(context.viewState.roomName, "Hello# Matrix!")
|
|
}
|
|
}
|