Files
element-x-ios/UnitTests/Sources/SearchScreenViewModelTests.swift
T
Stefan Ceriu 82e666bfe1 Only show joined rooms in the search results
Rooms the user is invited to, or has knocked on, are handled by the chats
list and aren't somewhere search should lead. The search screen now asks
the room summary provider for joined rooms only.
2026-09-16 11:37:11 +03:00

189 lines
7.5 KiB
Swift

//
// 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.
//
import Combine
@testable import ElementX
import Testing
@MainActor
struct SearchScreenViewModelTests {
let viewModel: SearchScreenViewModelProtocol
let searchService: SearchServiceProxyMock
let userIndicatorController: UserIndicatorControllerMock
let appSettings: AppSettings
/// The rooms known to the client, empty until the room list loads.
let staticRoomListSubject = CurrentValueSubject<[RoomSummary], Never>([])
/// Fires with the query each time the (async, debounced) message search runs.
let setQuerySubject = PassthroughSubject<String, Never>()
/// Fires each time an indicator is submitted.
let submitIndicatorSubject = PassthroughSubject<Void, Never>()
var context: SearchScreenViewModelType.Context {
viewModel.context
}
init() {
searchService = SearchServiceProxyMock()
searchService.underlyingResultsPublisher = CurrentValueSubject<[SearchServiceResult], Never>([]).asCurrentValuePublisher()
searchService.underlyingPaginationStatePublisher = CurrentValueSubject(.idle(endReached: true)).asCurrentValuePublisher()
searchService.setQueryClosure = { [setQuerySubject] query in
setQuerySubject.send(query)
return .success(())
}
userIndicatorController = UserIndicatorControllerMock()
userIndicatorController.submitIndicatorDelayClosure = { [submitIndicatorSubject] _, _ in
submitIndicatorSubject.send(())
}
let clientProxy = ClientProxyMock(.init())
clientProxy.searchService = searchService
let staticRoomSummaryProvider = RoomSummaryProviderMock()
staticRoomSummaryProvider.roomListPublisher = staticRoomListSubject.asCurrentValuePublisher()
clientProxy.staticRoomSummaryProvider = staticRoomSummaryProvider
clientProxy.roomSummaryForIdentifierClosure = { [staticRoomListSubject] identifier in
staticRoomListSubject.value.first { $0.id == identifier }
}
appSettings = AppSettings.volatile()
viewModel = SearchScreenViewModel(roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))),
clientProxy: clientProxy,
mediaProvider: MediaProviderMock(.init()),
userIndicatorController: userIndicatorController,
appSettings: appSettings)
}
@Test
func searching() async throws {
let deferred = deferFulfillment(context.observe(\.viewState.rooms)) { $0.count == 1 }
context.searchQuery = "Second"
try await deferred.fulfill()
// The rooms tab must not trigger the slow message search.
#expect(!searchService.setQueryCalled)
}
@Test
func searchingExcludesRoomsThatAreNotJoined() async throws {
let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms + .mockInvites)))
let clientProxy = ClientProxyMock(.init())
clientProxy.searchService = searchService
let viewModel = SearchScreenViewModel(roomSummaryProvider: roomSummaryProvider,
clientProxy: clientProxy,
mediaProvider: MediaProviderMock(.init()),
userIndicatorController: userIndicatorController,
appSettings: appSettings)
let deferred = deferFulfillment(viewModel.context.observe(\.viewState.rooms)) { !$0.isEmpty }
viewModel.context.searchQuery = "room"
try await deferred.fulfill()
#expect(roomSummaryProvider.setFilterReceivedFilter == .search(query: "room", joinedOnly: true))
#expect(!viewModel.context.viewState.rooms.contains { $0.title == "First room" })
}
@Test
func messageSearch() async throws {
let deferred = deferFulfillment(setQuerySubject) { $0 == "Foundation" }
context.searchMode = .messages
context.searchQuery = "Foundation"
try await deferred.fulfill()
#expect(searchService.setQueryReceivedQuery == "Foundation")
}
@Test
func messageSearchFailureShowsErrorIndicator() async throws {
searchService.setQueryClosure = { _ in .failure(.sdkError(SearchScreenViewModelTestsError.failed)) }
let deferred = deferFulfillment(submitIndicatorSubject) { _ in true }
context.searchMode = .messages
context.searchQuery = "Foundation"
try await deferred.fulfill()
#expect(userIndicatorController.submitIndicatorDelayCalled)
}
@Test
func roomSelection() async throws {
let deferred = deferFulfillment(viewModel.actionsPublisher) { action in
switch action {
case .presentRoom(let roomID, _):
roomID == "2"
case .cancel:
false
}
}
context.send(viewAction: .selectRoom(roomID: "2"))
try await deferred.fulfill()
}
@Test
func selectionRecordsBreadcrumbs() {
context.searchQuery = "Second"
context.send(viewAction: .selectRoom(roomID: "2"))
#expect(appSettings.searchBreadcrumbs == [.room(roomID: "2"), .query("Second")])
// Selecting the same room again from the empty state only bumps it back to the top.
context.searchQuery = ""
context.send(viewAction: .selectRoom(roomID: "2"))
#expect(appSettings.searchBreadcrumbs == [.room(roomID: "2"), .query("Second")])
}
@Test
func breadcrumbsAreDisplayed() async throws {
staticRoomListSubject.send([.mock(id: "2", name: "Second")])
let deferred = deferFulfillment(context.observe(\.viewState.breadcrumbs)) { $0.count == 2 }
appSettings.searchBreadcrumbs = [.query("Second"), .room(roomID: "2")]
try await deferred.fulfill()
#expect(context.viewState.breadcrumbs.map(\.id) == ["query-Second", "room-2"])
}
/// The room list is empty when the app launches, so the rooms need resolving again once it has loaded.
@Test
func roomBreadcrumbsAreDisplayedAfterTheRoomListLoads() async throws {
var deferred = deferFulfillment(context.observe(\.viewState.breadcrumbs)) { $0.count == 1 }
appSettings.searchBreadcrumbs = [.query("Second"), .room(roomID: "2")]
try await deferred.fulfill()
#expect(context.viewState.breadcrumbs.map(\.id) == ["query-Second"])
deferred = deferFulfillment(context.observe(\.viewState.breadcrumbs)) { $0.count == 2 }
staticRoomListSubject.send([.mock(id: "2", name: "Second")])
try await deferred.fulfill()
#expect(context.viewState.breadcrumbs.map(\.id) == ["query-Second", "room-2"])
}
@Test
func cancel() async throws {
let deferred = deferFulfillment(viewModel.actionsPublisher) { action in
switch action {
case .cancel:
true
default:
false
}
}
context.send(viewAction: .cancel)
try await deferred.fulfill()
}
}
private enum SearchScreenViewModelTestsError: Error {
case failed
}