mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
c41a38159b
* Initial plan * Offer only joined rooms in destination pickers Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com> * Clean whitespace in share picker test Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com> * Restrict the shared picker room list to joined rooms The alternate room summary provider, which backs the forward, share and Add rooms pickers and global search, now always includes the SDK's joined filter in its base filter. This replaces the per-call joinedOnly flags, so Add rooms no longer offers invited rooms or spaces and no screen can forget the flag. A filter set before the provider has its room list is now applied once the list is set, instead of being reset to the default. The share picker hit this on a cold launch from the share extension. The forward and share pickers also hide upgraded rooms, matching the Suggestions section. The SDK has no filter for these. Also registers the share picker's test file in the project, fixes the blank-line indentation SwiftFormat flagged, and re-records the picker snapshots without the upgraded room. * Check room membership for forwarding suggestions and search breadcrumbs RoomSummary now has isJoined, built from the room's membership. The forwarding suggestions and the search breadcrumbs used a missing joinRequestType to mean joined, which is also the case for rooms that have been left or banned from. It only worked because the room list's base filter already drops those. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com> Co-authored-by: Mauro Romito <mauro.romito@element.io>
303 lines
13 KiB
Swift
303 lines
13 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
|
|
@testable import ElementX
|
|
import MatrixRustSDKMocks
|
|
import Testing
|
|
|
|
@MainActor
|
|
struct MessageForwardingScreenViewModelTests {
|
|
let forwardingPayload = MessageForwardingPayload(ids: [.event(uniqueID: .init("t1"), eventOrTransactionID: .eventID("t1")),
|
|
.event(uniqueID: .init("t2"), eventOrTransactionID: .eventID("t2"))],
|
|
roomID: "1",
|
|
contents: [RoomMessageEventContentWithoutRelationSDKMock(),
|
|
RoomMessageEventContentWithoutRelationSDKMock()])
|
|
var viewModel: MessageForwardingScreenViewModelProtocol!
|
|
var context: MessageForwardingScreenViewModelType.Context!
|
|
|
|
init() {
|
|
let clientProxy = ClientProxyMock(.init())
|
|
clientProxy.roomForIdentifierClosure = { .joined(JoinedRoomProxyMock(.init(id: $0))) }
|
|
|
|
viewModel = MessageForwardingScreenViewModel(forwardingPayload: forwardingPayload,
|
|
userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
|
roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))),
|
|
userIndicatorController: UserIndicatorControllerMock())
|
|
context = viewModel.context
|
|
}
|
|
|
|
@Test
|
|
func initialState() {
|
|
#expect(!context.viewState.rooms.contains { $0.id == forwardingPayload.roomID }, "The source room ID shouldn't be shown")
|
|
#expect(!context.viewState.rooms.contains { $0.id == "7" }, "Upgraded rooms shouldn't be shown")
|
|
}
|
|
|
|
@Test
|
|
mutating func suggestionsShowRecentlyVisitedRooms() async throws {
|
|
viewModel = makeViewModel(recentlyVisitedRoomIDs: ["3", "5", "2"])
|
|
context = viewModel.context
|
|
|
|
let deferred = deferFulfillment(context.$viewState) { !$0.suggestedRooms.isEmpty }
|
|
try await deferred.fulfill()
|
|
|
|
#expect(context.viewState.suggestedRooms.map(\.id) == ["3", "5", "2"])
|
|
}
|
|
|
|
@Test
|
|
mutating func suggestionsOnlyIncludeRoomsThatCanBeForwardedTo() async throws {
|
|
viewModel = makeViewModel(recentlyVisitedRoomIDs: [forwardingPayload.roomID, "space", "7", "someAwesomeRoomId1", "left", "!unknown:matrix.org", "2"])
|
|
context = viewModel.context
|
|
|
|
let deferred = deferFulfillment(context.$viewState) { !$0.suggestedRooms.isEmpty }
|
|
try await deferred.fulfill()
|
|
|
|
#expect(context.viewState.suggestedRooms.map(\.id) == ["2"])
|
|
}
|
|
|
|
@Test
|
|
mutating func suggestionsAreLimitedToFiveRooms() async throws {
|
|
viewModel = makeViewModel(recentlyVisitedRoomIDs: [forwardingPayload.roomID, "7", "someAwesomeRoomId1", "2", "3", "4", "5", "6", "0"])
|
|
context = viewModel.context
|
|
|
|
let deferred = deferFulfillment(context.$viewState) { !$0.suggestedRooms.isEmpty }
|
|
try await deferred.fulfill()
|
|
|
|
#expect(context.viewState.suggestedRooms.map(\.id) == ["2", "3", "4", "5", "6"])
|
|
}
|
|
|
|
@Test
|
|
mutating func suggestedRoomsAreNotListedInChats() async throws {
|
|
viewModel = makeViewModel(recentlyVisitedRoomIDs: ["2", "5"])
|
|
context = viewModel.context
|
|
|
|
let deferred = deferFulfillment(context.$viewState) { !$0.suggestedRooms.isEmpty }
|
|
try await deferred.fulfill()
|
|
|
|
let chatIDs = context.viewState.chats.map(\.id)
|
|
#expect(!chatIDs.contains("2") && !chatIDs.contains("5"))
|
|
#expect(chatIDs.contains("3"))
|
|
}
|
|
|
|
@Test
|
|
mutating func searchingListsSuggestedRoomsInChats() async throws {
|
|
viewModel = makeViewModel(recentlyVisitedRoomIDs: ["2"])
|
|
context = viewModel.context
|
|
|
|
let deferredSuggestions = deferFulfillment(context.$viewState) { !$0.suggestedRooms.isEmpty }
|
|
try await deferredSuggestions.fulfill()
|
|
|
|
let deferredSearch = deferFulfillment(context.$viewState) { $0.rooms.count == 1 }
|
|
context.searchQuery = "Empire"
|
|
try await deferredSearch.fulfill()
|
|
|
|
#expect(context.viewState.chats.map(\.id) == ["2"])
|
|
}
|
|
|
|
@Test
|
|
mutating func searchingHidesTheSuggestions() async throws {
|
|
viewModel = makeViewModel(recentlyVisitedRoomIDs: ["2"])
|
|
context = viewModel.context
|
|
|
|
let deferredSuggestions = deferFulfillment(context.$viewState) { !$0.suggestedRooms.isEmpty }
|
|
try await deferredSuggestions.fulfill()
|
|
#expect(context.viewState.showsSuggestions)
|
|
|
|
let deferredSearch = deferFulfillment(context.$viewState) { $0.rooms.count == 1 }
|
|
context.searchQuery = "Second"
|
|
try await deferredSearch.fulfill()
|
|
#expect(!context.viewState.showsSuggestions)
|
|
#expect(context.viewState.suggestedRooms.map(\.id) == ["2"], "The suggestions shouldn't be searched")
|
|
|
|
context.searchQuery = ""
|
|
#expect(context.viewState.showsSuggestions)
|
|
}
|
|
|
|
@Test
|
|
mutating func noSuggestionsAreShownWhenNoRecentRoomCanBeSuggested() async throws {
|
|
viewModel = makeViewModel(recentlyVisitedRoomIDs: [forwardingPayload.roomID, "7"])
|
|
context = viewModel.context
|
|
|
|
let deferred = deferFailure(context.$viewState.map(\.showsSuggestions), timeout: .seconds(1)) { $0 }
|
|
try await deferred.fulfill()
|
|
}
|
|
|
|
@Test
|
|
mutating func noSuggestionsAreShownWhenRecentRoomsFailToLoad() async throws {
|
|
let clientProxy = ClientProxyMock(.init())
|
|
clientProxy.recentlyVisitedRoomIDsReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
|
|
viewModel = MessageForwardingScreenViewModel(forwardingPayload: forwardingPayload,
|
|
userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
|
roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))),
|
|
userIndicatorController: UserIndicatorControllerMock())
|
|
context = viewModel.context
|
|
|
|
let deferred = deferFailure(context.$viewState.map(\.showsSuggestions), timeout: .seconds(1)) { $0 }
|
|
try await deferred.fulfill()
|
|
}
|
|
|
|
@Test
|
|
mutating func roomSelection() {
|
|
context.send(viewAction: .selectRoom(roomID: "2"))
|
|
#expect(context.viewState.selectedRoomIDs == ["2"])
|
|
}
|
|
|
|
@Test
|
|
mutating func searching() async throws {
|
|
let deferred = deferFulfillment(context.$viewState) { state in
|
|
state.rooms.count == 1
|
|
}
|
|
|
|
context.searchQuery = "Second"
|
|
|
|
try await deferred.fulfill()
|
|
}
|
|
|
|
@Test
|
|
mutating func forwarding() async throws {
|
|
context.send(viewAction: .selectRoom(roomID: "2"))
|
|
#expect(context.viewState.selectedRoomIDs == ["2"])
|
|
|
|
let deferred = deferFulfillment(viewModel.actions) { action in
|
|
switch action {
|
|
case .sent(let roomIDs):
|
|
return roomIDs == ["2"]
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
context.send(viewAction: .send)
|
|
|
|
try await deferred.fulfill()
|
|
}
|
|
|
|
@Test
|
|
mutating func forwardingSendsEveryMessageToEveryRoomInOrder() async throws {
|
|
let clientProxy = ClientProxyMock(.init())
|
|
var roomProxies = [String: JoinedRoomProxyMock]()
|
|
clientProxy.roomForIdentifierClosure = { roomID in
|
|
let roomProxy = JoinedRoomProxyMock(.init(id: roomID))
|
|
roomProxies[roomID] = roomProxy
|
|
return .joined(roomProxy)
|
|
}
|
|
viewModel = MessageForwardingScreenViewModel(forwardingPayload: forwardingPayload,
|
|
userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
|
roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))),
|
|
userIndicatorController: UserIndicatorControllerMock())
|
|
context = viewModel.context
|
|
|
|
context.send(viewAction: .selectRoom(roomID: "2"))
|
|
context.send(viewAction: .selectRoom(roomID: "3"))
|
|
|
|
let deferred = deferFulfillment(viewModel.actions) { action in
|
|
if case .sent(let roomIDs) = action {
|
|
return Set(roomIDs) == ["2", "3"]
|
|
}
|
|
return false
|
|
}
|
|
context.send(viewAction: .send)
|
|
try await deferred.fulfill()
|
|
|
|
let expectedContents = forwardingPayload.contents.map { ObjectIdentifier($0) }
|
|
for roomID in ["2", "3"] {
|
|
let timeline = try #require(roomProxies[roomID]?.timeline as? TimelineProxyMock)
|
|
let sentContents = timeline.sendMessageEventContentReceivedInvocations.map { ObjectIdentifier($0) }
|
|
#expect(sentContents == expectedContents)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
mutating func forwardingToSeveralRoomsShowsAToast() async throws {
|
|
let userIndicatorController = UserIndicatorControllerMock()
|
|
viewModel = makeViewModel(userIndicatorController: userIndicatorController)
|
|
context = viewModel.context
|
|
|
|
context.send(viewAction: .selectRoom(roomID: "2"))
|
|
context.send(viewAction: .selectRoom(roomID: "3"))
|
|
|
|
let deferred = deferFulfillment(viewModel.actions) { action in
|
|
if case .sent = action {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
context.send(viewAction: .send)
|
|
try await deferred.fulfill()
|
|
|
|
let titles = userIndicatorController.submitIndicatorDelayReceivedInvocations.map(\.indicator.title)
|
|
#expect(titles == [L10n.screenRoomMessagesForwarded(2)])
|
|
}
|
|
|
|
@Test
|
|
mutating func forwardingToOneRoomDoesNotShowAToast() async throws {
|
|
let userIndicatorController = UserIndicatorControllerMock()
|
|
viewModel = makeViewModel(userIndicatorController: userIndicatorController)
|
|
context = viewModel.context
|
|
|
|
context.send(viewAction: .selectRoom(roomID: "2"))
|
|
|
|
let deferred = deferFulfillment(viewModel.actions) { action in
|
|
if case .sent = action {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
context.send(viewAction: .send)
|
|
try await deferred.fulfill()
|
|
|
|
// The flow opens the room instead.
|
|
#expect(userIndicatorController.submitIndicatorDelayCallsCount == 0)
|
|
}
|
|
|
|
@Test
|
|
mutating func forwardingReportsRoomsThatCouldNotBeReached() async throws {
|
|
let clientProxy = ClientProxyMock(.init())
|
|
clientProxy.roomForIdentifierClosure = { roomID in
|
|
roomID == "3" ? nil : .joined(JoinedRoomProxyMock(.init(id: roomID)))
|
|
}
|
|
let userIndicatorController = UserIndicatorControllerMock()
|
|
viewModel = MessageForwardingScreenViewModel(forwardingPayload: forwardingPayload,
|
|
userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
|
roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))),
|
|
userIndicatorController: userIndicatorController)
|
|
context = viewModel.context
|
|
|
|
context.send(viewAction: .selectRoom(roomID: "2"))
|
|
context.send(viewAction: .selectRoom(roomID: "3"))
|
|
|
|
let deferred = deferFulfillment(viewModel.actions) { action in
|
|
if case .sent(let roomIDs) = action {
|
|
return roomIDs == ["2"]
|
|
}
|
|
return false
|
|
}
|
|
context.send(viewAction: .send)
|
|
try await deferred.fulfill()
|
|
|
|
#expect(userIndicatorController.submitIndicatorDelayCallsCount == 1)
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private func makeViewModel(recentlyVisitedRoomIDs: [String] = [],
|
|
userIndicatorController: UserIndicatorControllerProtocol = UserIndicatorControllerMock()) -> MessageForwardingScreenViewModelProtocol {
|
|
let clientProxy = ClientProxyMock(.init())
|
|
clientProxy.roomForIdentifierClosure = { .joined(JoinedRoomProxyMock(.init(id: $0))) }
|
|
clientProxy.recentlyVisitedRoomIDsReturnValue = .success(recentlyVisitedRoomIDs)
|
|
let knownRooms: [RoomSummary] = .mockRooms + .mockInvites + [.mock(id: "space", name: "Space", isSpace: true),
|
|
.mock(id: "left", name: "Left", isJoined: false)]
|
|
clientProxy.roomSummaryForIdentifierClosure = { roomID in knownRooms.first { $0.id == roomID } }
|
|
|
|
return MessageForwardingScreenViewModel(forwardingPayload: forwardingPayload,
|
|
userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
|
roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))),
|
|
userIndicatorController: userIndicatorController)
|
|
}
|
|
}
|