refactor: manage member sheet in timeline

better implementation

updated tests

Update ElementX/Sources/Screens/Timeline/TimelineViewModel.swift

Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>

fix
This commit is contained in:
Mauro Romito
2025-04-08 15:23:40 +02:00
committed by Mauro
parent 56476f8bb5
commit 0e7533145a
44 changed files with 760 additions and 344 deletions
@@ -0,0 +1,100 @@
//
// Copyright 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 XCTest
@testable import ElementX
@MainActor
class ManageRoomMemberSheetViewModelTests: XCTestCase {
private var viewModel: ManageRoomMemberSheetViewModel!
private var context: ManageRoomMemberSheetViewModel.Context! {
viewModel.context
}
func testKick() async throws {
let testReason = "Kick Test"
let roomProxy = JoinedRoomProxyMock(.init(members: [RoomMemberProxyMock.mockAdmin, RoomMemberProxyMock.mockAlice]))
let expectation = XCTestExpectation(description: "Kick member")
roomProxy.kickUserReasonClosure = { userID, reason in
defer { expectation.fulfill() }
XCTAssertEqual(userID, RoomMemberProxyMock.mockAlice.userID)
XCTAssertEqual(reason, testReason)
return .success(())
}
viewModel = ManageRoomMemberSheetViewModel(member: RoomMemberDetails(withProxy: RoomMemberProxyMock.mockAlice),
canKick: true,
canBan: true,
roomProxy: roomProxy,
userIndicatorController: UserIndicatorControllerMock(),
analyticsService: ServiceLocator.shared.analytics,
mediaProvider: MediaProviderMock(configuration: .init()))
let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
let deferredAction = deferFulfillment(viewModel.actions) { action in
action == .dismiss(shouldShowDetails: false)
}
context.send(viewAction: .kick)
try await deferred.fulfill()
context.alertInfo?.textFields?[0].text.wrappedValue = testReason
context.alertInfo?.secondaryButton?.action?()
await fulfillment(of: [expectation])
try await deferredAction.fulfill()
}
func testBan() async throws {
let testReason = "Ban Test"
let roomProxy = JoinedRoomProxyMock(.init(members: [RoomMemberProxyMock.mockAdmin, RoomMemberProxyMock.mockAlice]))
let expectation = XCTestExpectation(description: "Ban member")
roomProxy.banUserReasonClosure = { userID, reason in
defer { expectation.fulfill() }
XCTAssertEqual(userID, RoomMemberProxyMock.mockAlice.userID)
XCTAssertEqual(reason, testReason)
return .success(())
}
viewModel = ManageRoomMemberSheetViewModel(member: RoomMemberDetails(withProxy: RoomMemberProxyMock.mockAlice),
canKick: true,
canBan: true,
roomProxy: roomProxy,
userIndicatorController: UserIndicatorControllerMock(),
analyticsService: ServiceLocator.shared.analytics,
mediaProvider: MediaProviderMock(configuration: .init()))
let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
context.send(viewAction: .ban)
try await deferred.fulfill()
let deferredAction = deferFulfillment(viewModel.actions) { action in
action == .dismiss(shouldShowDetails: false)
}
context.alertInfo?.textFields?[0].text.wrappedValue = testReason
context.alertInfo?.secondaryButton?.action?()
await fulfillment(of: [expectation])
try await deferredAction.fulfill()
}
func testDisplayDetails() async throws {
let roomProxy = JoinedRoomProxyMock(.init(members: [RoomMemberProxyMock.mockAdmin, RoomMemberProxyMock.mockAlice]))
viewModel = ManageRoomMemberSheetViewModel(member: RoomMemberDetails(withProxy: RoomMemberProxyMock.mockAlice),
canKick: true,
canBan: true,
roomProxy: roomProxy,
userIndicatorController: UserIndicatorControllerMock(),
analyticsService: ServiceLocator.shared.analytics,
mediaProvider: MediaProviderMock(configuration: .init()))
let deferredAction = deferFulfillment(viewModel.actions) { action in
action == .dismiss(shouldShowDetails: true)
}
context.send(viewAction: .displayDetails)
try await deferredAction.fulfill()
XCTAssertNil(context.alertInfo)
}
}
@@ -143,7 +143,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
// Then the member's details should be shown.
try await memberDetailsAction.fulfill()
XCTAssertNil(context.memberToManage)
XCTAssertNil(context.manageMemeberViewModel)
}
func testSelectUserAsAdmin() async throws {
@@ -151,10 +151,10 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
setup(with: .allMembersAsAdmin)
var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers }
try await deferred.fulfill()
XCTAssertNil(context.memberToManage)
XCTAssertNil(context.manageMemeberViewModel)
// When tapping on a user in the list.
deferred = deferFulfillment(context.$viewState) { $0.bindings.memberToManage != nil }
deferred = deferFulfillment(context.$viewState) { $0.bindings.manageMemeberViewModel != nil }
guard let user = viewModel.state.visibleJoinedMembers.first(where: { $0.member.role == .user && $0.member.id != RoomMemberProxyMock.mockMe.userID })?.member else {
XCTFail("Expected to find a regular user.")
return
@@ -163,8 +163,9 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
try await deferred.fulfill()
// Then member management should be shown for that user.
XCTAssertEqual(context.memberToManage?.member, user)
XCTAssertEqual(context.memberToManage?.actions, [.kick, .ban])
XCTAssertEqual(context.manageMemeberViewModel?.state.member, user)
XCTAssertEqual(context.manageMemeberViewModel?.state.canKick, true)
XCTAssertEqual(context.manageMemeberViewModel?.state.canBan, true)
}
func testSelectModeratorAsAdmin() async throws {
@@ -172,10 +173,10 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
setup(with: .allMembersAsAdmin)
var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers }
try await deferred.fulfill()
XCTAssertNil(context.memberToManage)
XCTAssertNil(context.manageMemeberViewModel)
// When tapping on a moderator in the list.
deferred = deferFulfillment(context.$viewState) { $0.bindings.memberToManage != nil }
deferred = deferFulfillment(context.$viewState) { $0.bindings.manageMemeberViewModel != nil }
guard let moderator = viewModel.state.visibleJoinedMembers.first(where: { $0.member.role == .moderator })?.member else {
XCTFail("Expected to find a moderator.")
return
@@ -184,8 +185,9 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
try await deferred.fulfill()
// Then member management should be shown for the moderator.
XCTAssertEqual(context.memberToManage?.member, moderator)
XCTAssertEqual(context.memberToManage?.actions, [.kick, .ban])
XCTAssertEqual(context.manageMemeberViewModel?.state.member, moderator)
XCTAssertEqual(context.manageMemeberViewModel?.state.canKick, true)
XCTAssertEqual(context.manageMemeberViewModel?.state.canBan, true)
}
func testSelectAdminAsAdmin() async throws {
@@ -204,7 +206,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
// Then the administrator's details should be shown.
try await memberDetailsAction.fulfill()
XCTAssertNil(context.memberToManage)
XCTAssertNil(context.manageMemeberViewModel)
}
func testSelectOwnMemberAsAdmin() async throws {
@@ -223,7 +225,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
// Then your member's details should be shown.
try await memberDetailsAction.fulfill()
XCTAssertNil(context.memberToManage)
XCTAssertNil(context.manageMemeberViewModel)
}
func testSelectBannedMember() async throws {
@@ -243,50 +245,10 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
// Then an alert should be shown to unban the user.
try await deferred.fulfill()
XCTAssertNil(context.memberToManage)
XCTAssertNil(context.manageMemeberViewModel)
XCTAssertNotNil(context.alertInfo)
}
func testKickMember() async throws {
setup(with: .allMembersAsAdmin)
var deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty }
try await deferred.fulfill()
let expectation = XCTestExpectation(description: "Ban member")
roomProxy.kickUserReasonClosure = { _, _ in
defer { expectation.fulfill() }
return .success(())
}
context.send(viewAction: .kickMember(viewModel.state.visibleJoinedMembers[0].member))
deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
try await deferred.fulfill()
context.alertInfo?.secondaryButton?.action?()
await fulfillment(of: [expectation])
}
func testBanMember() async throws {
setup(with: .allMembersAsAdmin)
var deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty }
try await deferred.fulfill()
let expectation = XCTestExpectation(description: "Ban member")
roomProxy.banUserReasonClosure = { _, _ in
defer { expectation.fulfill() }
return .success(())
}
context.send(viewAction: .banMember(viewModel.state.visibleJoinedMembers[0].member))
deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
try await deferred.fulfill()
context.alertInfo?.secondaryButton?.action?()
await fulfillment(of: [expectation])
}
func testUnbanMember() async throws {
setup(with: .allMembersAsAdmin)
let deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty }
@@ -350,6 +350,117 @@ class TimelineViewModelTests: XCTestCase {
try await deferred.fulfill()
}
func testShowManageUserAsAdmin() async throws {
let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "",
members: [RoomMemberProxyMock.mockAdmin,
RoomMemberProxyMock.mockAlice],
ownUserID: RoomMemberProxyMock.mockAdmin.userID)),
timelineController: MockTimelineController(),
mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
userIndicatorController: userIndicatorControllerMock,
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
clientProxy: ClientProxyMock(.init()))
var deferred = deferFulfillment(viewModel.context.$viewState) { value in
value.canCurrentUserKick && value.canCurrentUserBan
}
try await deferred.fulfill()
deferred = deferFulfillment(viewModel.context.$viewState) { value in
value.bindings.manageMemberViewModel != nil
}
viewModel.context.send(viewAction: .tappedOnSenderDetails(userID: RoomMemberProxyMock.mockAlice.userID))
try await deferred.fulfill()
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.member.id, RoomMemberProxyMock.mockAlice.userID)
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.canBan, true)
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.canKick, true)
}
func testShowDetailsForAnAdmin() async throws {
let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "",
members: [RoomMemberProxyMock.mockAdmin,
RoomMemberProxyMock.mockAlice],
ownUserID: RoomMemberProxyMock.mockAlice.userID)),
timelineController: MockTimelineController(),
mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
userIndicatorController: userIndicatorControllerMock,
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
clientProxy: ClientProxyMock(.init()))
let deferredState = deferFulfillment(viewModel.context.$viewState) { value in
!value.canCurrentUserKick && !value.canCurrentUserBan
}
try await deferredState.fulfill()
let deferredAction = deferFulfillment(viewModel.actions) { action in
switch action {
case .displaySenderDetails(let userID):
return userID == RoomMemberProxyMock.mockAdmin.userID
default:
return false
}
}
viewModel.context.send(viewAction: .tappedOnSenderDetails(userID: RoomMemberProxyMock.mockAdmin.userID))
try await deferredAction.fulfill()
XCTAssertNil(viewModel.context.manageMemberViewModel)
}
func testShowDetailsForABannedUser() async throws {
let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "",
members: [RoomMemberProxyMock.mockAdmin,
RoomMemberProxyMock.mockBanned[0]],
ownUserID: RoomMemberProxyMock.mockAdmin.userID)),
timelineController: MockTimelineController(),
mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
userIndicatorController: userIndicatorControllerMock,
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
clientProxy: ClientProxyMock(.init()))
let deferredState = deferFulfillment(viewModel.context.$viewState) { value in
value.canCurrentUserKick && value.canCurrentUserBan
}
try await deferredState.fulfill()
let deferredAction = deferFulfillment(viewModel.actions) { action in
switch action {
case .displaySenderDetails(let userID):
return userID == RoomMemberProxyMock.mockBanned[0].userID
default:
return false
}
}
viewModel.context.send(viewAction: .tappedOnSenderDetails(userID: RoomMemberProxyMock.mockBanned[0].userID))
try await deferredAction.fulfill()
XCTAssertNil(viewModel.context.manageMemberViewModel)
}
// MARK: - Pins
func testPinnedEvents() async throws {