Add tests

This commit is contained in:
Jorge Martín
2026-08-20 14:18:06 +02:00
parent 3ca5081f33
commit 8bdce7cfd6
3 changed files with 247 additions and 1 deletions
@@ -18,9 +18,11 @@ import io.element.android.features.home.impl.datasource.aRoomListRoomSummaryFact
import io.element.android.features.home.impl.filters.RoomListFiltersState
import io.element.android.features.home.impl.filters.aRoomListFiltersState
import io.element.android.features.home.impl.model.createRoomListRoomSummary
import io.element.android.features.home.impl.search.GlobalSearchPresenter
import io.element.android.features.home.impl.search.RoomListSearchEvent
import io.element.android.features.home.impl.search.RoomListSearchState
import io.element.android.features.home.impl.search.aRoomListSearchState
import io.element.android.features.home.impl.search.createGlobalSearchPresenter
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
import io.element.android.features.home.impl.spacefilters.aDisabledSpaceFiltersState
import io.element.android.features.invite.api.SeenInvitesStore
@@ -80,6 +82,7 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
@@ -680,6 +683,12 @@ class RoomListPresenterTest {
analyticsService = FakeAnalyticsService(),
),
searchPresenter = searchPresenter,
globalSearchPresenterFactory = object : GlobalSearchPresenter.Factory {
override fun create(coroutineScope: CoroutineScope) = createGlobalSearchPresenter(
matrixClient = client,
coroutineScope = coroutineScope,
)
},
filtersPresenter = filtersPresenter,
spaceFiltersPresenter = spaceFiltersPresenter,
analyticsService = analyticsService,
@@ -277,7 +277,7 @@ private fun AndroidComposeUiTest<ComponentActivity>.setRoomListView(
setSafeContent {
HomeView(
homeState = aHomeState(roomListState = state),
onRoomClick = onRoomClick,
onRoomClick = { roomId, _ -> onRoomClick(roomId) },
onSettingsClick = onSettingsClick,
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
@@ -0,0 +1,237 @@
/*
* Copyright (c) 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.
*/
package io.element.android.features.home.impl.search
import com.google.common.truth.Truth.assertThat
import io.element.android.features.home.impl.datasource.aRoomListRoomSummaryFactory
import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter
import io.element.android.libraries.eventformatter.test.FakeRoomLatestEventFormatter
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.search.MessageSearchService
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.libraries.matrix.test.roomlist.FakeDynamicRoomList
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.matrix.test.search.FakeMessageSearch
import io.element.android.libraries.matrix.test.search.FakeMessageSearchService
import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.util.Optional
class GlobalSearchPresenterTest {
@Test
fun `present - initial state`() = runTest {
val presenter = createGlobalSearchPresenter()
presenter.test {
awaitItem().let { state ->
assertThat(state.isEnabled).isFalse()
assertThat(state.isSearchActive).isFalse()
assertThat(state.queryState.text.toString()).isEmpty()
assertThat(state.currentTarget).isEqualTo(GlobalSearchTarget.ROOMS)
assertThat(state.results).isEqualTo(AsyncData.Uninitialized)
}
}
}
@Test
fun `present - isEnabled reflects the feature flag`() = runTest {
val presenter = createGlobalSearchPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.MessageSearch.key to true),
),
)
presenter.test {
// isEnabled is emitted asynchronously via produceState, so it starts as false and flips to true
var state = awaitItem()
while (!state.isEnabled) {
state = awaitItem()
}
assertThat(state.isEnabled).isTrue()
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - toggle search visibility`() = runTest {
val presenter = createGlobalSearchPresenter()
presenter.test {
awaitItem().let { state ->
assertThat(state.isSearchActive).isFalse()
state.eventSink(GlobalSearchEvent.ToggleSearchVisibility)
}
awaitItem().let { state ->
assertThat(state.isSearchActive).isTrue()
state.eventSink(GlobalSearchEvent.ToggleSearchVisibility)
}
awaitItem().let { state ->
assertThat(state.isSearchActive).isFalse()
}
}
}
@Test
fun `present - update target`() = runTest {
val presenter = createGlobalSearchPresenter()
presenter.test {
awaitItem().let { state ->
assertThat(state.currentTarget).isEqualTo(GlobalSearchTarget.ROOMS)
state.eventSink(GlobalSearchEvent.UpdateTarget(GlobalSearchTarget.MESSAGES))
}
awaitItem().let { state ->
assertThat(state.currentTarget).isEqualTo(GlobalSearchTarget.MESSAGES)
}
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - clear query`() = runTest {
val presenter = createGlobalSearchPresenter()
presenter.test {
val initialState = awaitItem()
initialState.queryState.edit { append("A query") }
awaitItem().let { state ->
assertThat(state.queryState.text.toString()).isEqualTo("A query")
state.eventSink(GlobalSearchEvent.ClearQuery)
}
awaitItem().let { state ->
assertThat(state.queryState.text.toString()).isEmpty()
}
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - room search results are emitted when the room list changes`() = runTest {
val roomList = FakeDynamicRoomList()
val roomListService = FakeRoomListService(createRoomListLambda = { roomList })
val presenter = createGlobalSearchPresenter(roomListService = roomListService)
presenter.test {
val initialState = awaitItem()
assertThat(initialState.results).isEqualTo(AsyncData.Uninitialized)
initialState.queryState.edit { append("A query") }
// Let the query be propagated to the data source before emitting results
testScheduler.advanceUntilIdle()
roomList.summaries.emit(listOf(aRoomSummary()))
val successState = consumeItemsUntilPredicate { it.results is AsyncData.Success }.last()
val results = (successState.results.dataOrNull() as GlobalSearchResults.RoomListResults).results
assertThat(results).hasSize(1)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - message search results are mapped when the message search emits`() = runTest {
val messageSearch = FakeMessageSearch()
val matrixClient = FakeMatrixClient().apply {
getRoomInfoFlowLambda = { flowOf(Optional.of(aRoomInfo())) }
}
val presenter = createGlobalSearchPresenter(
messageSearchService = FakeMessageSearchService(messageSearch),
matrixClient = matrixClient,
)
presenter.test {
val initialState = awaitItem()
initialState.eventSink(GlobalSearchEvent.UpdateTarget(GlobalSearchTarget.MESSAGES))
initialState.queryState.edit { append("A query") }
testScheduler.advanceUntilIdle()
messageSearch.emitResults(persistentListOf(aMessageSearchResult()))
testScheduler.advanceUntilIdle()
val successState = consumeItemsUntilPredicate { it.results is AsyncData.Success }.last()
val results = (successState.results.dataOrNull() as GlobalSearchResults.MessageSearchResults).results
assertThat(results).hasSize(1)
assertThat(results.first()).isInstanceOf(MessageSearchResultItem.Message::class.java)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - UpdateVisibleRange triggers pagination for messages when near the end`() = runTest {
val messageSearch = FakeMessageSearch()
val matrixClient = FakeMatrixClient().apply {
getRoomInfoFlowLambda = { flowOf(Optional.of(aRoomInfo())) }
}
val presenter = createGlobalSearchPresenter(
messageSearchService = FakeMessageSearchService(messageSearch),
matrixClient = matrixClient,
)
presenter.test {
val initialState = awaitItem()
initialState.eventSink(GlobalSearchEvent.UpdateTarget(GlobalSearchTarget.MESSAGES))
initialState.queryState.edit { append("A query") }
testScheduler.advanceUntilIdle()
messageSearch.emitResults(persistentListOf(aMessageSearchResult()))
val successState = consumeItemsUntilPredicate { it.results is AsyncData.Success }.last()
successState.eventSink(GlobalSearchEvent.UpdateVisibleRange(IntRange(0, 0)))
runCurrent()
testScheduler.advanceUntilIdle()
assertThat(messageSearch.paginateCallCount).isEqualTo(1)
cancelAndIgnoreRemainingEvents()
}
}
}
internal fun TestScope.createGlobalSearchPresenter(
roomListService: RoomListService = FakeRoomListService(),
messageSearchService: MessageSearchService = FakeMessageSearchService(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
latestEventFormatter: RoomLatestEventFormatter = FakeRoomLatestEventFormatter(),
dateFormatter: DateFormatter = FakeDateFormatter(),
fileSizeFormatter: FileSizeFormatter = FakeFileSizeFormatter(),
permalinkParser: PermalinkParser = FakePermalinkParser(),
matrixClient: MatrixClient = FakeMatrixClient(),
coroutineScope: CoroutineScope = backgroundScope,
): GlobalSearchPresenter {
return GlobalSearchPresenter(
coroutineScope = coroutineScope,
roomListSearchDataSourceFactory = object : RoomListSearchDataSource.Factory {
override fun create(coroutineScope: CoroutineScope): RoomListSearchDataSource {
return RoomListSearchDataSource(
coroutineScope = coroutineScope,
roomListService = roomListService,
coroutineDispatchers = testCoroutineDispatchers(),
roomSummaryFactory = aRoomListRoomSummaryFactory(),
)
}
},
messageSearchService = messageSearchService,
featureFlagService = featureFlagService,
latestEventFormatter = latestEventFormatter,
dateFormatter = dateFormatter,
fileSizeFormatter = fileSizeFormatter,
permalinkParser = permalinkParser,
coroutineDispatchers = testCoroutineDispatchers(),
matrixClient = matrixClient,
)
}