Add tests and clean code after RoomList Filter rework

This commit is contained in:
ganfra
2026-01-30 15:03:50 +01:00
parent 0824a3ab8b
commit 9641d3ef4f
27 changed files with 325 additions and 161 deletions

View File

@@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
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.roomlist.FakeRoomListService
import io.element.android.libraries.matrix.ui.room.LoadingRoomState import io.element.android.libraries.matrix.ui.room.LoadingRoomState
import io.element.android.libraries.matrix.ui.room.LoadingRoomStateFlowFactory import io.element.android.libraries.matrix.ui.room.LoadingRoomStateFlowFactory
@@ -54,7 +55,8 @@ class LoadingBaseRoomStateFlowFactoryTest {
@Test @Test
fun `flow should emit Loading and then Loaded when there is a room in cache after SS is loaded`() = runTest { fun `flow should emit Loading and then Loaded when there is a room in cache after SS is loaded`() = runTest {
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(sessionId = A_SESSION_ID, roomId = A_ROOM_ID)) val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(sessionId = A_SESSION_ID, roomId = A_ROOM_ID))
val roomListService = FakeRoomListService() val roomList = FakeDynamicRoomList()
val roomListService = FakeRoomListService(allRooms = roomList)
val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService) val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService)
val flowFactory = LoadingRoomStateFlowFactory(matrixClient) val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
flowFactory flowFactory
@@ -62,21 +64,22 @@ class LoadingBaseRoomStateFlowFactoryTest {
.test { .test {
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading) assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
matrixClient.givenGetRoomResult(A_ROOM_ID, room) matrixClient.givenGetRoomResult(A_ROOM_ID, room)
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) roomList.loadingState.emit(RoomList.LoadingState.Loaded(1))
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loaded(room)) assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loaded(room))
} }
} }
@Test @Test
fun `flow should emit Loading and then Error when there is no room in cache after SS is loaded`() = runTest { fun `flow should emit Loading and then Error when there is no room in cache after SS is loaded`() = runTest {
val roomListService = FakeRoomListService() val roomList = FakeDynamicRoomList()
val roomListService = FakeRoomListService(allRooms = roomList)
val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService) val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService)
val flowFactory = LoadingRoomStateFlowFactory(matrixClient) val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
flowFactory flowFactory
.create(lifecycleScope = this, roomId = A_ROOM_ID, joinedRoom = null) .create(lifecycleScope = this, roomId = A_ROOM_ID, joinedRoom = null)
.test { .test {
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading) assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) roomList.loadingState.emit(RoomList.LoadingState.Loaded(1))
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Error) assertThat(awaitItem()).isEqualTo(LoadingRoomState.Error)
} }
} }

View File

@@ -23,9 +23,7 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource

View File

@@ -17,9 +17,7 @@ import io.element.android.features.home.impl.filters.selection.FilterSelectionSt
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlin.time.Duration.Companion.milliseconds
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter
@Inject @Inject

View File

@@ -317,6 +317,4 @@ class RoomListPresenter(
room.clearEventCacheStorage() room.clearEventCacheStorage()
} }
} }
} }

View File

@@ -66,5 +66,4 @@ class RoomListSearchDataSource(
} }
roomList.updateFilter(filter) roomList.updateFilter(filter)
} }
} }

View File

@@ -16,9 +16,11 @@ import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.room.aRoomSummary 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.roomlist.FakeRoomListService
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@@ -27,9 +29,13 @@ import java.time.Instant
class RoomListDataSourceTest { class RoomListDataSourceTest {
@Test @Test
fun `when DateTimeObserver gets a date change, the room summaries are refreshed`() = runTest { fun `when DateTimeObserver gets a date change, the room summaries are refreshed`() = runTest {
val roomListService = FakeRoomListService().apply { val roomList = FakeDynamicRoomList().apply {
summaries.emit(listOf(aRoomSummary()))
}
val roomListService = FakeRoomListService(
createRoomListLambda = { roomList }
).apply {
postState(RoomListService.State.Running) postState(RoomListService.State.Running)
postAllRooms(listOf(aRoomSummary()))
} }
val dateTimeObserver = FakeDateTimeObserver() val dateTimeObserver = FakeDateTimeObserver()
var dateFormatterResult = "Today" var dateFormatterResult = "Today"
@@ -61,9 +67,11 @@ class RoomListDataSourceTest {
@Test @Test
fun `when DateTimeObserver gets a time zone change, the room summaries are refreshed`() = runTest { fun `when DateTimeObserver gets a time zone change, the room summaries are refreshed`() = runTest {
val roomListService = FakeRoomListService().apply { val roomList = FakeDynamicRoomList(summaries = MutableStateFlow(listOf(aRoomSummary())))
val roomListService = FakeRoomListService(
createRoomListLambda = { roomList }
).apply {
postState(RoomListService.State.Running) postState(RoomListService.State.Running)
postAllRooms(listOf(aRoomSummary()))
} }
val dateTimeObserver = FakeDateTimeObserver() val dateTimeObserver = FakeDateTimeObserver()
var dateFormatterResult = "Today" var dateFormatterResult = "Today"

View File

@@ -31,7 +31,6 @@ import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter
class RoomListFiltersPresenterTest { class RoomListFiltersPresenterTest {
@Test @Test

View File

@@ -55,6 +55,7 @@ import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.test.room.aRoomSummary 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.roomlist.FakeRoomListService
import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
@@ -77,6 +78,7 @@ import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.advanceTimeBy
@@ -91,7 +93,10 @@ class RoomListPresenterTest {
@Test @Test
fun `present - load 1 room with success`() = runTest { fun `present - load 1 room with success`() = runTest {
val roomListService = FakeRoomListService() val roomList = FakeDynamicRoomList()
val roomListService = FakeRoomListService(
createRoomListLambda = { roomList }
)
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
roomListService = roomListService roomListService = roomListService
) )
@@ -102,8 +107,8 @@ class RoomListPresenterTest {
presenter.test { presenter.test {
val initialState = consumeItemsUntilPredicate { state -> state.contentState is RoomListContentState.Skeleton }.last() val initialState = consumeItemsUntilPredicate { state -> state.contentState is RoomListContentState.Skeleton }.last()
assertThat(initialState.contentState).isInstanceOf(RoomListContentState.Skeleton::class.java) assertThat(initialState.contentState).isInstanceOf(RoomListContentState.Skeleton::class.java)
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) roomList.loadingState.emit(RoomList.LoadingState.Loaded(1))
roomListService.postAllRooms( roomList.summaries.emit(
listOf( listOf(
aRoomSummary( aRoomSummary(
numUnreadMentions = 1, numUnreadMentions = 1,
@@ -128,9 +133,12 @@ class RoomListPresenterTest {
@Test @Test
fun `present - handle DismissRequestVerificationPrompt`() = runTest { fun `present - handle DismissRequestVerificationPrompt`() = runTest {
val roomListService = FakeRoomListService().apply { val roomList = FakeDynamicRoomList(
postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1))
} )
val roomListService = FakeRoomListService(
createRoomListLambda = { roomList }
)
val encryptionService = FakeEncryptionService().apply { val encryptionService = FakeEncryptionService().apply {
emitRecoveryState(RecoveryState.INCOMPLETE) emitRecoveryState(RecoveryState.INCOMPLETE)
} }
@@ -154,9 +162,12 @@ class RoomListPresenterTest {
val encryptionService = FakeEncryptionService().apply { val encryptionService = FakeEncryptionService().apply {
recoveryStateStateFlow.emit(RecoveryState.DISABLED) recoveryStateStateFlow.emit(RecoveryState.DISABLED)
} }
val roomListService = FakeRoomListService().apply { val roomList = FakeDynamicRoomList(
postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1))
} )
val roomListService = FakeRoomListService(
createRoomListLambda = { roomList }
)
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
roomListService = roomListService, roomListService = roomListService,
encryptionService = encryptionService, encryptionService = encryptionService,
@@ -344,9 +355,13 @@ class RoomListPresenterTest {
fun `present - change in notification settings updates the summary for decorations`() = runTest { fun `present - change in notification settings updates the summary for decorations`() = runTest {
val userDefinedMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY val userDefinedMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
val notificationSettingsService = FakeNotificationSettingsService() val notificationSettingsService = FakeNotificationSettingsService()
val roomListService = FakeRoomListService() val roomList = FakeDynamicRoomList(
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) summaries = MutableStateFlow(listOf(aRoomSummary(userDefinedNotificationMode = userDefinedMode))),
roomListService.postAllRooms(listOf(aRoomSummary(userDefinedNotificationMode = userDefinedMode))) loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1))
)
val roomListService = FakeRoomListService(
createRoomListLambda = { roomList }
)
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
roomListService = roomListService, roomListService = roomListService,
notificationSettingsService = notificationSettingsService notificationSettingsService = notificationSettingsService
@@ -397,8 +412,12 @@ class RoomListPresenterTest {
@Test @Test
fun `present - when room service returns no room, then contentState is Empty`() = runTest { fun `present - when room service returns no room, then contentState is Empty`() = runTest {
val roomListService = FakeRoomListService() val roomList = FakeDynamicRoomList(
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(0)) loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(0))
)
val roomListService = FakeRoomListService(
createRoomListLambda = { roomList }
)
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
roomListService = roomListService, roomListService = roomListService,
) )
@@ -479,16 +498,21 @@ class RoomListPresenterTest {
val acceptDeclinePresenter = Presenter { val acceptDeclinePresenter = Presenter {
anAcceptDeclineInviteState(eventSink = eventSinkRecorder) anAcceptDeclineInviteState(eventSink = eventSinkRecorder)
} }
val roomListService = FakeRoomListService()
val matrixClient = FakeMatrixClient(
roomListService = roomListService,
)
val roomSummary = aRoomSummary( val roomSummary = aRoomSummary(
currentUserMembership = CurrentUserMembership.INVITED, currentUserMembership = CurrentUserMembership.INVITED,
inviter = aRoomMember(), inviter = aRoomMember(),
) )
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) val roomList = FakeDynamicRoomList(
roomListService.postAllRooms(listOf(roomSummary)) summaries = MutableStateFlow(listOf(roomSummary)),
loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1))
)
val roomListService = FakeRoomListService(
createRoomListLambda = { roomList }
)
val matrixClient = FakeMatrixClient(
roomListService = roomListService,
)
val presenter = createRoomListPresenter( val presenter = createRoomListPresenter(
client = matrixClient, client = matrixClient,
acceptDeclineInvitePresenter = acceptDeclinePresenter acceptDeclineInvitePresenter = acceptDeclinePresenter
@@ -519,15 +543,20 @@ class RoomListPresenterTest {
@Test @Test
fun `present - UpdateVisibleRange will cancel the previous subscription if called too soon`() = runTest { fun `present - UpdateVisibleRange will cancel the previous subscription if called too soon`() = runTest {
val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> } val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> }
val roomListService = FakeRoomListService(subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda)
val matrixClient = FakeMatrixClient(
roomListService = roomListService,
)
val roomSummary = aRoomSummary( val roomSummary = aRoomSummary(
currentUserMembership = CurrentUserMembership.INVITED currentUserMembership = CurrentUserMembership.INVITED
) )
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) val roomList = FakeDynamicRoomList(
roomListService.postAllRooms(listOf(roomSummary)) summaries = MutableStateFlow(listOf(roomSummary)),
loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1))
)
val roomListService = FakeRoomListService(
createRoomListLambda = { roomList },
subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda,
)
val matrixClient = FakeMatrixClient(
roomListService = roomListService,
)
val presenter = createRoomListPresenter( val presenter = createRoomListPresenter(
client = matrixClient, client = matrixClient,
) )
@@ -548,15 +577,20 @@ class RoomListPresenterTest {
@Test @Test
fun `present - UpdateVisibleRange subscribes to rooms in visible range`() = runTest { fun `present - UpdateVisibleRange subscribes to rooms in visible range`() = runTest {
val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> } val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> }
val roomListService = FakeRoomListService(subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda)
val matrixClient = FakeMatrixClient(
roomListService = roomListService,
)
val roomSummary = aRoomSummary( val roomSummary = aRoomSummary(
currentUserMembership = CurrentUserMembership.INVITED currentUserMembership = CurrentUserMembership.INVITED
) )
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) val roomList = FakeDynamicRoomList(
roomListService.postAllRooms(listOf(roomSummary)) summaries = MutableStateFlow(listOf(roomSummary)),
loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1))
)
val roomListService = FakeRoomListService(
createRoomListLambda = { roomList },
subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda,
)
val matrixClient = FakeMatrixClient(
roomListService = roomListService,
)
val presenter = createRoomListPresenter( val presenter = createRoomListPresenter(
client = matrixClient, client = matrixClient,
) )
@@ -579,15 +613,20 @@ class RoomListPresenterTest {
@Test @Test
fun `present - notification sound banner`() = runTest { fun `present - notification sound banner`() = runTest {
val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> } val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> }
val roomListService = FakeRoomListService(subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda)
val matrixClient = FakeMatrixClient(
roomListService = roomListService,
)
val roomSummary = aRoomSummary( val roomSummary = aRoomSummary(
currentUserMembership = CurrentUserMembership.INVITED currentUserMembership = CurrentUserMembership.INVITED
) )
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) val roomList = FakeDynamicRoomList(
roomListService.postAllRooms(listOf(roomSummary)) summaries = MutableStateFlow(listOf(roomSummary)),
loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1))
)
val roomListService = FakeRoomListService(
createRoomListLambda = { roomList },
subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda,
)
val matrixClient = FakeMatrixClient(
roomListService = roomListService,
)
val onAnnouncementDismissedResult = lambdaRecorder<Announcement, Unit> { } val onAnnouncementDismissedResult = lambdaRecorder<Announcement, Unit> { }
val announcementService = FakeAnnouncementService( val announcementService = FakeAnnouncementService(
onAnnouncementDismissedResult = onAnnouncementDismissedResult, onAnnouncementDismissedResult = onAnnouncementDismissedResult,

View File

@@ -15,7 +15,10 @@ import io.element.android.libraries.eventformatter.test.FakeRoomLatestEventForma
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.test.room.aRoomSummary 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.roomlist.FakeRoomListService
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.test import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -56,12 +59,15 @@ class RoomListSearchPresenterTest {
@Test @Test
fun `present - query search changes`() = runTest { fun `present - query search changes`() = runTest {
val roomListService = FakeRoomListService() val roomList = FakeDynamicRoomList()
val roomListService = FakeRoomListService(
createRoomListLambda = { roomList }
)
val presenter = createRoomListSearchPresenter(roomListService) val presenter = createRoomListSearchPresenter(roomListService)
presenter.test { presenter.test {
awaitItem().let { state -> awaitItem().let { state ->
assertThat( assertThat(
roomListService.allRooms.currentFilter.value roomList.currentFilter.value
).isEqualTo( ).isEqualTo(
RoomListFilter.None RoomListFilter.None
) )
@@ -70,7 +76,7 @@ class RoomListSearchPresenterTest {
awaitItem().let { state -> awaitItem().let { state ->
assertThat(state.query.text).isEqualTo("Search") assertThat(state.query.text).isEqualTo("Search")
assertThat( assertThat(
roomListService.allRooms.currentFilter.value roomList.currentFilter.value
).isEqualTo( ).isEqualTo(
RoomListFilter.NormalizedMatchRoomName("Search") RoomListFilter.NormalizedMatchRoomName("Search")
) )
@@ -79,7 +85,7 @@ class RoomListSearchPresenterTest {
awaitItem().let { state -> awaitItem().let { state ->
assertThat(state.query.text.toString()).isEmpty() assertThat(state.query.text.toString()).isEmpty()
assertThat( assertThat(
roomListService.allRooms.currentFilter.value roomList.currentFilter.value
).isEqualTo( ).isEqualTo(
RoomListFilter.None RoomListFilter.None
) )
@@ -89,24 +95,51 @@ class RoomListSearchPresenterTest {
@Test @Test
fun `present - room list changes`() = runTest { fun `present - room list changes`() = runTest {
val roomListService = FakeRoomListService() val roomList = FakeDynamicRoomList()
val roomListService = FakeRoomListService(
createRoomListLambda = { roomList }
)
val presenter = createRoomListSearchPresenter(roomListService) val presenter = createRoomListSearchPresenter(roomListService)
presenter.test { presenter.test {
awaitItem().let { state -> awaitItem().let { state ->
assertThat(state.results).isEmpty() assertThat(state.results).isEmpty()
} }
roomListService.postAllRooms( roomList.summaries.emit(
listOf(aRoomSummary()) listOf(aRoomSummary())
) )
awaitItem().let { state -> awaitItem().let { state ->
assertThat(state.results).hasSize(1) assertThat(state.results).hasSize(1)
} }
roomListService.postAllRooms(emptyList()) roomList.summaries.emit(emptyList())
awaitItem().let { state -> awaitItem().let { state ->
assertThat(state.results).isEmpty() assertThat(state.results).isEmpty()
} }
} }
} }
@Test
fun `present - UpdateVisibleRange triggers pagination when near end`() = runTest {
val loadMoreLambda = lambdaRecorder<Unit> { }
val roomList = FakeDynamicRoomList(loadMoreLambda = loadMoreLambda)
val roomListService = FakeRoomListService(
createRoomListLambda = { roomList }
)
val presenter = createRoomListSearchPresenter(roomListService)
presenter.test {
val initialState = awaitItem()
// Post some rooms to simulate loaded content
val rooms = (1..10).map { aRoomSummary() }
roomList.summaries.emit(rooms)
skipItems(1)
// UpdateVisibleRange near end should trigger loadMore
initialState.eventSink(RoomListSearchEvent.UpdateVisibleRange(IntRange(0, 9)))
// Give time for the coroutine to complete
testScheduler.advanceUntilIdle()
assert(loadMoreLambda).isCalledOnce()
}
}
} }
fun TestScope.createRoomListSearchPresenter( fun TestScope.createRoomListSearchPresenter(

View File

@@ -15,6 +15,7 @@ import io.element.android.features.messages.impl.messagecomposer.suggestions.Roo
import io.element.android.libraries.matrix.test.A_ROOM_ALIAS import io.element.android.libraries.matrix.test.A_ROOM_ALIAS
import io.element.android.libraries.matrix.test.A_ROOM_ID_2 import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.matrix.test.room.aRoomSummary 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.roomlist.FakeRoomListService
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@@ -22,7 +23,10 @@ import org.junit.Test
class DefaultRoomAliasSuggestionsDataSourceTest { class DefaultRoomAliasSuggestionsDataSourceTest {
@Test @Test
fun `getAllRoomAliasSuggestions must emit a list of room alias suggestions`() = runTest { fun `getAllRoomAliasSuggestions must emit a list of room alias suggestions`() = runTest {
val roomListService = FakeRoomListService() val roomList = FakeDynamicRoomList()
val roomListService = FakeRoomListService(
createRoomListLambda = { roomList }
)
val sut = DefaultRoomAliasSuggestionsDataSource( val sut = DefaultRoomAliasSuggestionsDataSource(
roomListService roomListService
) )
@@ -31,7 +35,7 @@ class DefaultRoomAliasSuggestionsDataSourceTest {
) )
sut.getAllRoomAliasSuggestions().test { sut.getAllRoomAliasSuggestions().test {
assertThat(awaitItem()).isEmpty() assertThat(awaitItem()).isEmpty()
roomListService.postAllRooms( roomList.summaries.emit(
listOf( listOf(
aRoomSummary(roomId = A_ROOM_ID_2, canonicalAlias = null), aRoomSummary(roomId = A_ROOM_ID_2, canonicalAlias = null),
aRoomSummaryWithAnAlias, aRoomSummaryWithAnAlias,

View File

@@ -17,10 +17,12 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2 import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.room.aRoomSummary 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.roomlist.FakeRoomListService
import io.element.android.tests.testutils.awaitLastSequentialItem import io.element.android.tests.testutils.awaitLastSequentialItem
import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.test import io.element.android.tests.testutils.test
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@@ -53,10 +55,14 @@ class EditDefaultNotificationSettingsPresenterTest {
initialRoomModeIsDefault = false, initialRoomModeIsDefault = false,
getRoomsWithUserDefinedRulesResult = { Result.success(listOf(A_ROOM_ID)) }, getRoomsWithUserDefinedRulesResult = { Result.success(listOf(A_ROOM_ID)) },
) )
val roomListService = FakeRoomListService() val roomList = FakeDynamicRoomList(
summaries = MutableStateFlow(listOf(aRoomSummary(userDefinedNotificationMode = RoomNotificationMode.ALL_MESSAGES)))
)
val roomListService = FakeRoomListService(
createRoomListLambda = { roomList }
)
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService) val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService)
presenter.test { presenter.test {
roomListService.postAllRooms(listOf(aRoomSummary(userDefinedNotificationMode = RoomNotificationMode.ALL_MESSAGES)))
val loadedState = consumeItemsUntilPredicate { state -> val loadedState = consumeItemsUntilPredicate { state ->
state.roomsWithUserDefinedMode.any { it.notificationMode == RoomNotificationMode.ALL_MESSAGES } state.roomsWithUserDefinedMode.any { it.notificationMode == RoomNotificationMode.ALL_MESSAGES }
}.last() }.last()
@@ -71,10 +77,8 @@ class EditDefaultNotificationSettingsPresenterTest {
initialRoomModeIsDefault = false, initialRoomModeIsDefault = false,
getRoomsWithUserDefinedRulesResult = { Result.success(listOf(A_ROOM_ID, A_ROOM_ID_2)) }, getRoomsWithUserDefinedRulesResult = { Result.success(listOf(A_ROOM_ID, A_ROOM_ID_2)) },
) )
val roomListService = FakeRoomListService() val roomList = FakeDynamicRoomList(
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService) summaries = MutableStateFlow(
presenter.test {
roomListService.postAllRooms(
listOf( listOf(
aRoomSummary( aRoomSummary(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@@ -86,8 +90,14 @@ class EditDefaultNotificationSettingsPresenterTest {
name = "A", name = "A",
userDefinedNotificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY, userDefinedNotificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
), ),
), )
) )
)
val roomListService = FakeRoomListService(
createRoomListLambda = { roomList }
)
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService)
presenter.test {
val loadedState = consumeItemsUntilPredicate { state -> val loadedState = consumeItemsUntilPredicate { state ->
state.roomsWithUserDefinedMode.any { it.notificationMode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY } state.roomsWithUserDefinedMode.any { it.notificationMode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY }
}.last() }.last()
@@ -103,10 +113,8 @@ class EditDefaultNotificationSettingsPresenterTest {
initialRoomModeIsDefault = false, initialRoomModeIsDefault = false,
getRoomsWithUserDefinedRulesResult = { Result.success(listOf(A_ROOM_ID, A_ROOM_ID_2)) }, getRoomsWithUserDefinedRulesResult = { Result.success(listOf(A_ROOM_ID, A_ROOM_ID_2)) },
) )
val roomListService = FakeRoomListService() val roomList = FakeDynamicRoomList(
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService) summaries = MutableStateFlow(
presenter.test {
roomListService.postAllRooms(
listOf( listOf(
aRoomSummary( aRoomSummary(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@@ -118,8 +126,14 @@ class EditDefaultNotificationSettingsPresenterTest {
name = null, name = null,
userDefinedNotificationMode = RoomNotificationMode.MUTE, userDefinedNotificationMode = RoomNotificationMode.MUTE,
), ),
), )
) )
)
val roomListService = FakeRoomListService(
createRoomListLambda = { roomList }
)
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService)
presenter.test {
val loadedState = consumeItemsUntilPredicate { state -> val loadedState = consumeItemsUntilPredicate { state ->
state.roomsWithUserDefinedMode.any { it.notificationMode == RoomNotificationMode.MUTE } state.roomsWithUserDefinedMode.any { it.notificationMode == RoomNotificationMode.MUTE }
}.last() }.last()

View File

@@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2 import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.aRoomSummary 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.roomlist.FakeRoomListService
import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
@@ -116,12 +117,15 @@ class AddRoomToSpacePresenterTest {
@Test @Test
fun `present - searchResults shows Results when rooms available`() = runTest { fun `present - searchResults shows Results when rooms available`() = runTest {
val roomListService = FakeRoomListService() val roomList = FakeDynamicRoomList()
val roomListService = FakeRoomListService(
createRoomListLambda = { roomList }
)
val presenter = createAddRoomToSpacePresenter(roomListService = roomListService) val presenter = createAddRoomToSpacePresenter(roomListService = roomListService)
presenter.test { presenter.test {
awaitItem() // Initial state awaitItem() // Initial state
// Post rooms to the service // Post rooms to the service
roomListService.postAllRooms( roomList.summaries.emit(
listOf( listOf(
aRoomSummary( aRoomSummary(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@@ -296,6 +300,29 @@ class AddRoomToSpacePresenterTest {
} }
} }
@Test
fun `present - UpdateSearchVisibleRange triggers pagination when near end`() = runTest {
val loadMoreLambda = lambdaRecorder<Unit> { }
val roomList = FakeDynamicRoomList(loadMoreLambda = loadMoreLambda)
val roomListService = FakeRoomListService(
createRoomListLambda = { roomList }
)
val presenter = createAddRoomToSpacePresenter(roomListService = roomListService)
presenter.test {
val state = awaitItem()
// Post rooms to simulate loaded content
roomList.summaries.emit(listOf(aRoomSummary()))
advanceUntilIdle()
skipItems(1)
// UpdateSearchVisibleRange should trigger loadMore
state.eventSink(AddRoomToSpaceEvent.UpdateSearchVisibleRange(IntRange(0, 9)))
advanceUntilIdle()
assert(loadMoreLambda).isCalledOnce()
}
}
@Test @Test
fun `present - Dismiss after partial success calls reset`() = runTest { fun `present - Dismiss after partial success calls reset`() = runTest {
val resetResult = lambdaRecorder<Result<Unit>> { Result.success(Unit) } val resetResult = lambdaRecorder<Result<Unit>> { Result.success(Unit) }

View File

@@ -14,6 +14,7 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
@@ -99,6 +100,21 @@ class AddRoomToSpaceViewTest {
) )
} }
} }
@Config(qualifiers = "h1024dp")
@Test
fun `displaying search results sends UpdateSearchVisibleRange event`() {
val eventsRecorder = EventsRecorder<AddRoomToSpaceEvent>()
val rooms = aSelectRoomInfoList()
rule.setAddRoomToSpaceView(
anAddRoomToSpaceState(
isSearchActive = true,
searchResults = SearchBarResultState.Results(rooms),
eventSink = eventsRecorder,
),
)
eventsRecorder.assertTrue(0) { it is AddRoomToSpaceEvent.UpdateSearchVisibleRange }
}
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAddRoomToSpaceView( private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAddRoomToSpaceView(

View File

@@ -12,17 +12,11 @@ import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.lazy.LazyListLayoutInfo import androidx.compose.foundation.lazy.LazyListLayoutInfo
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
/** /**
* Returns whether the lazy list is currently scrolling up. * Returns whether the lazy list is currently scrolling up.
@@ -79,20 +73,3 @@ suspend fun LazyListState.animateScrollToItemCenter(index: Int) {
animateScrollToItem(index, offset) animateScrollToItem(index, offset)
} }
} }
@Composable
fun OnVisibleRangeChangeEffect(lazyListState: LazyListState, onChange: (IntRange) -> Unit) {
val onChangeUpdated by rememberUpdatedState(onChange)
LaunchedEffect(lazyListState) {
snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo }
.map { visibleItemsInfo ->
val firstItemIndex = visibleItemsInfo.firstOrNull()?.index ?: 0
val size = visibleItemsInfo.size
firstItemIndex until firstItemIndex + size
}
.distinctUntilChanged()
.collectLatest { visibleRange ->
onChangeUpdated(visibleRange)
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* 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.libraries.designsystem.utils
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
@Composable
fun OnVisibleRangeChangeEffect(lazyListState: LazyListState, onChange: (IntRange) -> Unit) {
val onChangeUpdated by rememberUpdatedState(onChange)
LaunchedEffect(lazyListState) {
snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo }
.map { visibleItemsInfo ->
val firstItemIndex = visibleItemsInfo.firstOrNull()?.index ?: 0
val size = visibleItemsInfo.size
firstItemIndex until firstItemIndex + size
}
.distinctUntilChanged()
.collectLatest { visibleRange ->
onChangeUpdated(visibleRange)
}
}
}

View File

@@ -8,8 +8,6 @@
package io.element.android.libraries.matrix.api.roomlist package io.element.android.libraries.matrix.api.roomlist
import timber.log.Timber
/** /**
* RoomList with dynamic filtering and loading. * RoomList with dynamic filtering and loading.
* This is useful for large lists of rooms. * This is useful for large lists of rooms.

View File

@@ -21,7 +21,6 @@ import kotlin.time.Duration
* Can be retrieved from [RoomListService] methods. * Can be retrieved from [RoomListService] methods.
*/ */
interface RoomList { interface RoomList {
/** /**
* The loading state of the room list. * The loading state of the room list.
*/ */

View File

@@ -18,13 +18,11 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListDynamicEntriesController import org.matrix.rustcomponents.sdk.RoomListDynamicEntriesController
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind
import org.matrix.rustcomponents.sdk.RoomListEntriesListener import org.matrix.rustcomponents.sdk.RoomListEntriesListener
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind
import org.matrix.rustcomponents.sdk.RoomListInterface import org.matrix.rustcomponents.sdk.RoomListInterface
import org.matrix.rustcomponents.sdk.RoomListLoadingState import org.matrix.rustcomponents.sdk.RoomListLoadingState
import org.matrix.rustcomponents.sdk.RoomListLoadingStateListener import org.matrix.rustcomponents.sdk.RoomListLoadingStateListener

View File

@@ -90,8 +90,6 @@ internal class RoomListFactory(
} }
} }
private fun RoomListLoadingState.toLoadingState(): RoomList.LoadingState { private fun RoomListLoadingState.toLoadingState(): RoomList.LoadingState {
return when (this) { return when (this) {
is RoomListLoadingState.Loaded -> RoomList.LoadingState.Loaded(maximumNumberOfRooms?.toInt() ?: 0) is RoomListLoadingState.Loaded -> RoomList.LoadingState.Loaded(maximumNumberOfRooms?.toInt() ?: 0)

View File

@@ -27,7 +27,6 @@ import org.matrix.rustcomponents.sdk.RoomListFilterCategory
* Mapper for converting RoomListFilter to Rust SDK filter kinds. * Mapper for converting RoomListFilter to Rust SDK filter kinds.
*/ */
internal object RoomListFilterMapper { internal object RoomListFilterMapper {
/** /**
* Base rust filters to always apply across all room lists. * Base rust filters to always apply across all room lists.
* These filters ensure we show: * These filters ensure we show:

View File

@@ -27,7 +27,6 @@ internal class RustDynamicRoomList(
private val dynamicController: () -> RoomListDynamicEntriesController?, private val dynamicController: () -> RoomListDynamicEntriesController?,
private val addPagesCount: Int = DEFAULT_ADD_PAGES_COUNT private val addPagesCount: Int = DEFAULT_ADD_PAGES_COUNT
) : DynamicRoomList { ) : DynamicRoomList {
private val mutex = Mutex() private val mutex = Mutex()
override suspend fun rebuildSummaries() { override suspend fun rebuildSummaries() {

View File

@@ -13,31 +13,30 @@ import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.getAndUpdate
data class SimplePagedRoomList( class FakeDynamicRoomList(
override val summaries: MutableStateFlow<List<RoomSummary>>, override val summaries: MutableStateFlow<List<RoomSummary>> = MutableStateFlow(emptyList()),
override val loadingState: StateFlow<RoomList.LoadingState>, override val loadingState: MutableStateFlow<RoomList.LoadingState> = MutableStateFlow(RoomList.LoadingState.NotLoaded),
private val currentFilter: MutableStateFlow<RoomListFilter> override val pageSize: Int = Int.MAX_VALUE,
val currentFilter: MutableStateFlow<RoomListFilter> = MutableStateFlow(RoomListFilter.None),
private val loadMoreLambda: () -> Unit = {},
private val resetLambda: () -> Unit = {},
private val updateFilterLambda: (RoomListFilter) -> Unit = { filter -> currentFilter.value = filter },
private val rebuildSummariesLambda: () -> Unit = {},
) : DynamicRoomList { ) : DynamicRoomList {
override val pageSize: Int = Int.MAX_VALUE
private val loadedPages = MutableStateFlow(1)
override suspend fun loadMore() { override suspend fun loadMore() {
// No-op loadMoreLambda()
loadedPages.getAndUpdate { it + 1 }
} }
override suspend fun reset() { override suspend fun reset() {
loadedPages.emit(1) resetLambda()
} }
override suspend fun updateFilter(filter: RoomListFilter) { override suspend fun updateFilter(filter: RoomListFilter) {
currentFilter.emit(filter) updateFilterLambda(filter)
} }
override suspend fun rebuildSummaries() { override suspend fun rebuildSummaries() {
// No-op rebuildSummariesLambda()
} }
} }

View File

@@ -11,29 +11,19 @@ package io.element.android.libraries.matrix.test.roomlist
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.roomlist.DynamicRoomList import io.element.android.libraries.matrix.api.roomlist.DynamicRoomList
import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
class FakeRoomListService( class FakeRoomListService(
var subscribeToVisibleRoomsLambda: (List<RoomId>) -> Unit = {}, private val subscribeToVisibleRoomsLambda: (List<RoomId>) -> Unit = {},
private val createRoomListLambda: (pageSize: Int) -> DynamicRoomList = { pageSize -> FakeDynamicRoomList(pageSize = pageSize) },
override val allRooms: RoomList = createRoomListLambda(Int.MAX_VALUE),
) : RoomListService { ) : RoomListService {
private val allRoomSummariesFlow = MutableStateFlow<List<RoomSummary>>(emptyList())
private val allRoomsLoadingStateFlow = MutableStateFlow<RoomList.LoadingState>(RoomList.LoadingState.NotLoaded)
private val roomListStateFlow = MutableStateFlow<RoomListService.State>(RoomListService.State.Idle) private val roomListStateFlow = MutableStateFlow<RoomListService.State>(RoomListService.State.Idle)
private val syncIndicatorStateFlow = MutableStateFlow<RoomListService.SyncIndicator>(RoomListService.SyncIndicator.Hide) private val syncIndicatorStateFlow = MutableStateFlow<RoomListService.SyncIndicator>(RoomListService.SyncIndicator.Hide)
suspend fun postAllRooms(roomSummaries: List<RoomSummary>) {
allRoomSummariesFlow.emit(roomSummaries)
}
suspend fun postAllRoomsLoadingState(loadingState: RoomList.LoadingState) {
allRoomsLoadingStateFlow.emit(loadingState)
}
suspend fun postState(state: RoomListService.State) { suspend fun postState(state: RoomListService.State) {
roomListStateFlow.emit(state) roomListStateFlow.emit(state)
} }
@@ -46,22 +36,12 @@ class FakeRoomListService(
pageSize: Int, pageSize: Int,
source: RoomList.Source, source: RoomList.Source,
coroutineScope: CoroutineScope, coroutineScope: CoroutineScope,
): DynamicRoomList { ) = createRoomListLambda(pageSize)
return when (source) {
RoomList.Source.All -> allRooms
}
}
override suspend fun subscribeToVisibleRooms(roomIds: List<RoomId>) { override suspend fun subscribeToVisibleRooms(roomIds: List<RoomId>) {
subscribeToVisibleRoomsLambda(roomIds) subscribeToVisibleRoomsLambda(roomIds)
} }
override val allRooms = SimplePagedRoomList(
allRoomSummariesFlow,
allRoomsLoadingStateFlow,
MutableStateFlow(RoomListFilter.all())
)
override val state: StateFlow<RoomListService.State> = roomListStateFlow override val state: StateFlow<RoomListService.State> = roomListStateFlow
override val syncIndicator: StateFlow<RoomListService.SyncIndicator> = syncIndicatorStateFlow override val syncIndicator: StateFlow<RoomListService.SyncIndicator> = syncIndicatorStateFlow

View File

@@ -16,6 +16,12 @@ plugins {
android { android {
namespace = "io.element.android.libraries.roomselect.impl" namespace = "io.element.android.libraries.roomselect.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
} }
setupDependencyInjection() setupDependencyInjection()
@@ -30,6 +36,6 @@ dependencies {
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
api(projects.libraries.roomselect.api) api(projects.libraries.roomselect.api)
testCommonDependencies(libs) testCommonDependencies(libs, includeTestComposeView = true)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
} }

View File

@@ -16,5 +16,5 @@ sealed interface RoomSelectEvents {
// TODO remove to restore multi-selection // TODO remove to restore multi-selection
data object RemoveSelectedRoom : RoomSelectEvents data object RemoveSelectedRoom : RoomSelectEvents
data object ToggleSearchActive : RoomSelectEvents data object ToggleSearchActive : RoomSelectEvents
data class UpdateVisibleRange(val range: IntRange): RoomSelectEvents data class UpdateVisibleRange(val range: IntRange) : RoomSelectEvents
} }

View File

@@ -43,22 +43,23 @@ open class RoomSelectStateProvider : PreviewParameterProvider<RoomSelectState> {
) )
} }
private fun aRoomSelectState( internal fun aRoomSelectState(
mode: RoomSelectMode = RoomSelectMode.Forward, mode: RoomSelectMode = RoomSelectMode.Forward,
resultState: SearchBarResultState<ImmutableList<SelectRoomInfo>> = SearchBarResultState.Initial(), resultState: SearchBarResultState<ImmutableList<SelectRoomInfo>> = SearchBarResultState.Initial(),
searchQuery: String = "", searchQuery: String = "",
isSearchActive: Boolean = false, isSearchActive: Boolean = false,
selectedRooms: ImmutableList<SelectRoomInfo> = persistentListOf(), selectedRooms: ImmutableList<SelectRoomInfo> = persistentListOf(),
eventSink: (RoomSelectEvents) -> Unit = {},
) = RoomSelectState( ) = RoomSelectState(
mode = mode, mode = mode,
resultState = resultState, resultState = resultState,
searchQuery = TextFieldState(initialText = searchQuery), searchQuery = TextFieldState(initialText = searchQuery),
isSearchActive = isSearchActive, isSearchActive = isSearchActive,
selectedRooms = selectedRooms, selectedRooms = selectedRooms,
eventSink = {} eventSink = eventSink,
) )
private fun aRoomSelectRoomList() = persistentListOf( internal fun aRoomSelectRoomList() = persistentListOf(
aSelectRoomInfo( aSelectRoomInfo(
roomId = RoomId("!room1:domain"), roomId = RoomId("!room1:domain"),
name = "Room with name", name = "Room with name",

View File

@@ -17,13 +17,17 @@ import io.element.android.libraries.designsystem.theme.components.SearchBarResul
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.test.room.aRoomSummary 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.roomlist.FakeRoomListService
import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo
import io.element.android.libraries.roomselect.api.RoomSelectMode import io.element.android.libraries.roomselect.api.RoomSelectMode
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
@@ -63,9 +67,12 @@ class RoomSelectPresenterTest {
@Test @Test
fun `present - update query`() = runTest { fun `present - update query`() = runTest {
val roomSummary = aRoomSummary() val roomSummary = aRoomSummary()
val roomListService = FakeRoomListService().apply { val roomList = FakeDynamicRoomList(
postAllRooms(listOf(roomSummary)) summaries = MutableStateFlow(listOf(roomSummary))
} )
val roomListService = FakeRoomListService(
createRoomListLambda = { roomList }
)
val presenter = createRoomSelectPresenter( val presenter = createRoomSelectPresenter(
roomListService = roomListService roomListService = roomListService
) )
@@ -81,12 +88,12 @@ class RoomSelectPresenterTest {
skipItems(1) skipItems(1)
initialState.searchQuery.setTextAndPlaceCursorAtEnd("string not contained") initialState.searchQuery.setTextAndPlaceCursorAtEnd("string not contained")
assertThat( assertThat(
roomListService.allRooms.currentFilter.value roomList.currentFilter.value
).isEqualTo( ).isEqualTo(
RoomListFilter.NormalizedMatchRoomName("string not contained") RoomListFilter.NormalizedMatchRoomName("string not contained")
) )
assertThat(awaitItem().searchQuery.text.toString()).isEqualTo("string not contained") assertThat(awaitItem().searchQuery.text.toString()).isEqualTo("string not contained")
roomListService.postAllRooms( roomList.summaries.emit(
emptyList() emptyList()
) )
assertThat(awaitItem().resultState).isInstanceOf(SearchBarResultState.NoResultsFound::class.java) assertThat(awaitItem().resultState).isInstanceOf(SearchBarResultState.NoResultsFound::class.java)
@@ -96,9 +103,12 @@ class RoomSelectPresenterTest {
@Test @Test
fun `present - select and remove a room`() = runTest { fun `present - select and remove a room`() = runTest {
val roomSummary = aRoomSummary() val roomSummary = aRoomSummary()
val roomListService = FakeRoomListService().apply { val roomList = FakeDynamicRoomList(
postAllRooms(listOf(roomSummary)) summaries = MutableStateFlow(listOf(roomSummary))
} )
val roomListService = FakeRoomListService(
createRoomListLambda = { roomList }
)
val presenter = createRoomSelectPresenter( val presenter = createRoomSelectPresenter(
roomListService = roomListService, roomListService = roomListService,
) )
@@ -114,6 +124,35 @@ class RoomSelectPresenterTest {
cancel() cancel()
} }
} }
@Test
fun `present - UpdateVisibleRange triggers pagination when near end`() = runTest {
val loadMoreLambda = lambdaRecorder<Unit> { }
val roomList = FakeDynamicRoomList(
summaries = MutableStateFlow(listOf()),
loadMoreLambda = loadMoreLambda,
)
val roomListService = FakeRoomListService(
createRoomListLambda = { roomList }
)
val presenter = createRoomSelectPresenter(roomListService = roomListService)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
// Post some rooms to simulate loaded content
val rooms = (1..10).map { aRoomSummary() }
roomList.summaries.emit(rooms)
skipItems(1)
// UpdateVisibleRange near end should trigger loadMore
initialState.eventSink(RoomSelectEvents.UpdateVisibleRange(IntRange(0, 9)))
// Give time for the coroutine to complete
testScheduler.advanceUntilIdle()
assert(loadMoreLambda).isCalledOnce()
}
}
} }
internal fun TestScope.createRoomSelectPresenter( internal fun TestScope.createRoomSelectPresenter(