From 9641d3ef4ff4db2467fca44594737d1447944cbd Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 30 Jan 2026 15:03:50 +0100 Subject: [PATCH] Add tests and clean code after RoomList Filter rework --- .../LoadingBaseRoomStateFlowFactoryTest.kt | 11 +- .../impl/components/RoomListContentView.kt | 2 - .../impl/filters/RoomListFiltersPresenter.kt | 2 - .../home/impl/roomlist/RoomListPresenter.kt | 2 - .../impl/search/RoomListSearchDataSource.kt | 1 - .../impl/datasource/RoomListDataSourceTest.kt | 16 ++- .../filters/RoomListFiltersPresenterTest.kt | 1 - .../impl/roomlist/RoomListPresenterTest.kt | 115 ++++++++++++------ .../search/RoomListSearchPresenterTest.kt | 47 +++++-- ...faultRoomAliasSuggestionsDataSourceTest.kt | 8 +- ...efaultNotificationSettingsPresenterTest.kt | 38 ++++-- .../addroom/AddRoomToSpacePresenterTest.kt | 31 ++++- .../impl/addroom/AddRoomToSpaceViewTest.kt | 16 +++ .../designsystem/utils/LazyListState.kt | 23 ---- .../utils/OnVisibleRangeChangeEffect.kt | 35 ++++++ .../matrix/api/roomlist/DynamicRoomList.kt | 2 - .../libraries/matrix/api/roomlist/RoomList.kt | 1 - .../impl/roomlist/RoomListExtensions.kt | 4 +- .../matrix/impl/roomlist/RoomListFactory.kt | 2 - .../impl/roomlist/RoomListFilterMapper.kt | 1 - .../impl/roomlist/RustDynamicRoomList.kt | 1 - ...agedRoomList.kt => FakeDynamicRoomList.kt} | 27 ++-- .../test/roomlist/FakeRoomListService.kt | 28 +---- libraries/roomselect/impl/build.gradle.kts | 8 +- .../roomselect/impl/RoomSelectEvents.kt | 2 +- .../impl/RoomSelectStateProvider.kt | 7 +- .../impl/RoomSelectPresenterTest.kt | 55 +++++++-- 27 files changed, 325 insertions(+), 161 deletions(-) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/OnVisibleRangeChangeEffect.kt rename libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/{SimplePagedRoomList.kt => FakeDynamicRoomList.kt} (52%) diff --git a/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingBaseRoomStateFlowFactoryTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingBaseRoomStateFlowFactoryTest.kt index 14128ac33a..7a02591237 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingBaseRoomStateFlowFactoryTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingBaseRoomStateFlowFactoryTest.kt @@ -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.room.FakeBaseRoom 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.ui.room.LoadingRoomState import io.element.android.libraries.matrix.ui.room.LoadingRoomStateFlowFactory @@ -54,7 +55,8 @@ class LoadingBaseRoomStateFlowFactoryTest { @Test 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 roomListService = FakeRoomListService() + val roomList = FakeDynamicRoomList() + val roomListService = FakeRoomListService(allRooms = roomList) val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService) val flowFactory = LoadingRoomStateFlowFactory(matrixClient) flowFactory @@ -62,21 +64,22 @@ class LoadingBaseRoomStateFlowFactoryTest { .test { assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading) 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)) } } @Test 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 flowFactory = LoadingRoomStateFlowFactory(matrixClient) flowFactory .create(lifecycleScope = this, roomId = A_ROOM_ID, joinedRoom = null) .test { assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading) - roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) + roomList.loadingState.emit(RoomList.LoadingState.Loaded(1)) assertThat(awaitItem()).isEqualTo(LoadingRoomState.Error) } } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt index c02efa02a1..f3628fce9d 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt @@ -23,9 +23,7 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt index 1b2b4d8b53..3c808045fd 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt @@ -17,9 +17,7 @@ import io.element.android.features.home.impl.filters.selection.FilterSelectionSt import io.element.android.libraries.architecture.Presenter import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.map -import kotlin.time.Duration.Companion.milliseconds import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter @Inject diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt index 60630ee63f..830a84ee4d 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt @@ -317,6 +317,4 @@ class RoomListPresenter( room.clearEventCacheStorage() } } - - } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchDataSource.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchDataSource.kt index bace8b9874..03fcb5520f 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchDataSource.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchDataSource.kt @@ -66,5 +66,4 @@ class RoomListSearchDataSource( } roomList.updateFilter(filter) } - } diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSourceTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSourceTest.kt index f76743e591..1ce5061356 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSourceTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSourceTest.kt @@ -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.test.notificationsettings.FakeNotificationSettingsService 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.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test @@ -27,9 +29,13 @@ import java.time.Instant class RoomListDataSourceTest { @Test 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) - postAllRooms(listOf(aRoomSummary())) } val dateTimeObserver = FakeDateTimeObserver() var dateFormatterResult = "Today" @@ -61,9 +67,11 @@ class RoomListDataSourceTest { @Test 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) - postAllRooms(listOf(aRoomSummary())) } val dateTimeObserver = FakeDateTimeObserver() var dateFormatterResult = "Today" diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenterTest.kt index 63e0814cd0..c30e279b2e 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenterTest.kt @@ -31,7 +31,6 @@ import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Test -import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter class RoomListFiltersPresenterTest { @Test diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt index dd2702bd77..9ee95cc811 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt @@ -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.aRoomMember 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.sync.FakeSyncService 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.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy @@ -91,7 +93,10 @@ class RoomListPresenterTest { @Test fun `present - load 1 room with success`() = runTest { - val roomListService = FakeRoomListService() + val roomList = FakeDynamicRoomList() + val roomListService = FakeRoomListService( + createRoomListLambda = { roomList } + ) val matrixClient = FakeMatrixClient( roomListService = roomListService ) @@ -102,8 +107,8 @@ class RoomListPresenterTest { presenter.test { val initialState = consumeItemsUntilPredicate { state -> state.contentState is RoomListContentState.Skeleton }.last() assertThat(initialState.contentState).isInstanceOf(RoomListContentState.Skeleton::class.java) - roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) - roomListService.postAllRooms( + roomList.loadingState.emit(RoomList.LoadingState.Loaded(1)) + roomList.summaries.emit( listOf( aRoomSummary( numUnreadMentions = 1, @@ -128,9 +133,12 @@ class RoomListPresenterTest { @Test fun `present - handle DismissRequestVerificationPrompt`() = runTest { - val roomListService = FakeRoomListService().apply { - postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) - } + val roomList = FakeDynamicRoomList( + loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1)) + ) + val roomListService = FakeRoomListService( + createRoomListLambda = { roomList } + ) val encryptionService = FakeEncryptionService().apply { emitRecoveryState(RecoveryState.INCOMPLETE) } @@ -154,9 +162,12 @@ class RoomListPresenterTest { val encryptionService = FakeEncryptionService().apply { recoveryStateStateFlow.emit(RecoveryState.DISABLED) } - val roomListService = FakeRoomListService().apply { - postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) - } + val roomList = FakeDynamicRoomList( + loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1)) + ) + val roomListService = FakeRoomListService( + createRoomListLambda = { roomList } + ) val matrixClient = FakeMatrixClient( roomListService = roomListService, encryptionService = encryptionService, @@ -344,9 +355,13 @@ class RoomListPresenterTest { fun `present - change in notification settings updates the summary for decorations`() = runTest { val userDefinedMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY val notificationSettingsService = FakeNotificationSettingsService() - val roomListService = FakeRoomListService() - roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) - roomListService.postAllRooms(listOf(aRoomSummary(userDefinedNotificationMode = userDefinedMode))) + val roomList = FakeDynamicRoomList( + summaries = MutableStateFlow(listOf(aRoomSummary(userDefinedNotificationMode = userDefinedMode))), + loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1)) + ) + val roomListService = FakeRoomListService( + createRoomListLambda = { roomList } + ) val matrixClient = FakeMatrixClient( roomListService = roomListService, notificationSettingsService = notificationSettingsService @@ -397,8 +412,12 @@ class RoomListPresenterTest { @Test fun `present - when room service returns no room, then contentState is Empty`() = runTest { - val roomListService = FakeRoomListService() - roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(0)) + val roomList = FakeDynamicRoomList( + loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(0)) + ) + val roomListService = FakeRoomListService( + createRoomListLambda = { roomList } + ) val matrixClient = FakeMatrixClient( roomListService = roomListService, ) @@ -479,16 +498,21 @@ class RoomListPresenterTest { val acceptDeclinePresenter = Presenter { anAcceptDeclineInviteState(eventSink = eventSinkRecorder) } - val roomListService = FakeRoomListService() - val matrixClient = FakeMatrixClient( - roomListService = roomListService, - ) + val roomSummary = aRoomSummary( currentUserMembership = CurrentUserMembership.INVITED, inviter = aRoomMember(), ) - roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) - roomListService.postAllRooms(listOf(roomSummary)) + val roomList = FakeDynamicRoomList( + summaries = MutableStateFlow(listOf(roomSummary)), + loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1)) + ) + val roomListService = FakeRoomListService( + createRoomListLambda = { roomList } + ) + val matrixClient = FakeMatrixClient( + roomListService = roomListService, + ) val presenter = createRoomListPresenter( client = matrixClient, acceptDeclineInvitePresenter = acceptDeclinePresenter @@ -519,15 +543,20 @@ class RoomListPresenterTest { @Test fun `present - UpdateVisibleRange will cancel the previous subscription if called too soon`() = runTest { val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List -> } - val roomListService = FakeRoomListService(subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda) - val matrixClient = FakeMatrixClient( - roomListService = roomListService, - ) val roomSummary = aRoomSummary( currentUserMembership = CurrentUserMembership.INVITED ) - roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) - roomListService.postAllRooms(listOf(roomSummary)) + val roomList = FakeDynamicRoomList( + 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( client = matrixClient, ) @@ -548,15 +577,20 @@ class RoomListPresenterTest { @Test fun `present - UpdateVisibleRange subscribes to rooms in visible range`() = runTest { val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List -> } - val roomListService = FakeRoomListService(subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda) - val matrixClient = FakeMatrixClient( - roomListService = roomListService, - ) val roomSummary = aRoomSummary( currentUserMembership = CurrentUserMembership.INVITED ) - roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) - roomListService.postAllRooms(listOf(roomSummary)) + val roomList = FakeDynamicRoomList( + 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( client = matrixClient, ) @@ -579,15 +613,20 @@ class RoomListPresenterTest { @Test fun `present - notification sound banner`() = runTest { val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List -> } - val roomListService = FakeRoomListService(subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda) - val matrixClient = FakeMatrixClient( - roomListService = roomListService, - ) val roomSummary = aRoomSummary( currentUserMembership = CurrentUserMembership.INVITED ) - roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) - roomListService.postAllRooms(listOf(roomSummary)) + val roomList = FakeDynamicRoomList( + 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 { } val announcementService = FakeAnnouncementService( onAnnouncementDismissedResult = onAnnouncementDismissedResult, diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenterTest.kt index c135d62e49..04a12f8591 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenterTest.kt @@ -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.RoomListService 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.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.testCoroutineDispatchers import kotlinx.coroutines.CoroutineScope @@ -56,12 +59,15 @@ class RoomListSearchPresenterTest { @Test fun `present - query search changes`() = runTest { - val roomListService = FakeRoomListService() + val roomList = FakeDynamicRoomList() + val roomListService = FakeRoomListService( + createRoomListLambda = { roomList } + ) val presenter = createRoomListSearchPresenter(roomListService) presenter.test { awaitItem().let { state -> assertThat( - roomListService.allRooms.currentFilter.value + roomList.currentFilter.value ).isEqualTo( RoomListFilter.None ) @@ -70,7 +76,7 @@ class RoomListSearchPresenterTest { awaitItem().let { state -> assertThat(state.query.text).isEqualTo("Search") assertThat( - roomListService.allRooms.currentFilter.value + roomList.currentFilter.value ).isEqualTo( RoomListFilter.NormalizedMatchRoomName("Search") ) @@ -79,7 +85,7 @@ class RoomListSearchPresenterTest { awaitItem().let { state -> assertThat(state.query.text.toString()).isEmpty() assertThat( - roomListService.allRooms.currentFilter.value + roomList.currentFilter.value ).isEqualTo( RoomListFilter.None ) @@ -89,24 +95,51 @@ class RoomListSearchPresenterTest { @Test fun `present - room list changes`() = runTest { - val roomListService = FakeRoomListService() + val roomList = FakeDynamicRoomList() + val roomListService = FakeRoomListService( + createRoomListLambda = { roomList } + ) val presenter = createRoomListSearchPresenter(roomListService) presenter.test { awaitItem().let { state -> assertThat(state.results).isEmpty() } - roomListService.postAllRooms( + roomList.summaries.emit( listOf(aRoomSummary()) ) awaitItem().let { state -> assertThat(state.results).hasSize(1) } - roomListService.postAllRooms(emptyList()) + roomList.summaries.emit(emptyList()) awaitItem().let { state -> assertThat(state.results).isEmpty() } } } + + @Test + fun `present - UpdateVisibleRange triggers pagination when near end`() = runTest { + val loadMoreLambda = lambdaRecorder { } + 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( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/DefaultRoomAliasSuggestionsDataSourceTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/DefaultRoomAliasSuggestionsDataSourceTest.kt index 5cd3607701..0c101a7440 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/DefaultRoomAliasSuggestionsDataSourceTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/DefaultRoomAliasSuggestionsDataSourceTest.kt @@ -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_ID_2 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 kotlinx.coroutines.test.runTest import org.junit.Test @@ -22,7 +23,10 @@ import org.junit.Test class DefaultRoomAliasSuggestionsDataSourceTest { @Test 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( roomListService ) @@ -31,7 +35,7 @@ class DefaultRoomAliasSuggestionsDataSourceTest { ) sut.getAllRoomAliasSuggestions().test { assertThat(awaitItem()).isEmpty() - roomListService.postAllRooms( + roomList.summaries.emit( listOf( aRoomSummary(roomId = A_ROOM_ID_2, canonicalAlias = null), aRoomSummaryWithAnAlias, diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTest.kt index 153c265560..e03b65f0a9 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTest.kt @@ -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.notificationsettings.FakeNotificationSettingsService 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.tests.testutils.awaitLastSequentialItem import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.test +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.Test @@ -53,10 +55,14 @@ class EditDefaultNotificationSettingsPresenterTest { initialRoomModeIsDefault = false, 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) presenter.test { - roomListService.postAllRooms(listOf(aRoomSummary(userDefinedNotificationMode = RoomNotificationMode.ALL_MESSAGES))) val loadedState = consumeItemsUntilPredicate { state -> state.roomsWithUserDefinedMode.any { it.notificationMode == RoomNotificationMode.ALL_MESSAGES } }.last() @@ -71,10 +77,8 @@ class EditDefaultNotificationSettingsPresenterTest { initialRoomModeIsDefault = false, getRoomsWithUserDefinedRulesResult = { Result.success(listOf(A_ROOM_ID, A_ROOM_ID_2)) }, ) - val roomListService = FakeRoomListService() - val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService) - presenter.test { - roomListService.postAllRooms( + val roomList = FakeDynamicRoomList( + summaries = MutableStateFlow( listOf( aRoomSummary( roomId = A_ROOM_ID, @@ -86,8 +90,14 @@ class EditDefaultNotificationSettingsPresenterTest { name = "A", userDefinedNotificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY, ), - ), + ) ) + ) + val roomListService = FakeRoomListService( + createRoomListLambda = { roomList } + ) + val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService) + presenter.test { val loadedState = consumeItemsUntilPredicate { state -> state.roomsWithUserDefinedMode.any { it.notificationMode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY } }.last() @@ -103,10 +113,8 @@ class EditDefaultNotificationSettingsPresenterTest { initialRoomModeIsDefault = false, getRoomsWithUserDefinedRulesResult = { Result.success(listOf(A_ROOM_ID, A_ROOM_ID_2)) }, ) - val roomListService = FakeRoomListService() - val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService) - presenter.test { - roomListService.postAllRooms( + val roomList = FakeDynamicRoomList( + summaries = MutableStateFlow( listOf( aRoomSummary( roomId = A_ROOM_ID, @@ -118,8 +126,14 @@ class EditDefaultNotificationSettingsPresenterTest { name = null, userDefinedNotificationMode = RoomNotificationMode.MUTE, ), - ), + ) ) + ) + val roomListService = FakeRoomListService( + createRoomListLambda = { roomList } + ) + val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService) + presenter.test { val loadedState = consumeItemsUntilPredicate { state -> state.roomsWithUserDefinedMode.any { it.notificationMode == RoomNotificationMode.MUTE } }.last() diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenterTest.kt index 381206c3ec..cdfd4e548c 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenterTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenterTest.kt @@ -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.FakeMatrixClient 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.spaces.FakeSpaceRoomList import io.element.android.libraries.matrix.test.spaces.FakeSpaceService @@ -116,12 +117,15 @@ class AddRoomToSpacePresenterTest { @Test 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) presenter.test { awaitItem() // Initial state // Post rooms to the service - roomListService.postAllRooms( + roomList.summaries.emit( listOf( aRoomSummary( roomId = A_ROOM_ID, @@ -296,6 +300,29 @@ class AddRoomToSpacePresenterTest { } } + @Test + fun `present - UpdateSearchVisibleRange triggers pagination when near end`() = runTest { + val loadMoreLambda = lambdaRecorder { } + 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 fun `present - Dismiss after partial success calls reset`() = runTest { val resetResult = lambdaRecorder> { Result.success(Unit) } diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceViewTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceViewTest.kt index 16710ae0e4..d75fecd05a 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceViewTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceViewTest.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 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.tests.testutils.EnsureNeverCalled 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() + val rooms = aSelectRoomInfoList() + rule.setAddRoomToSpaceView( + anAddRoomToSpaceState( + isSearchActive = true, + searchResults = SearchBarResultState.Results(rooms), + eventSink = eventsRecorder, + ), + ) + eventsRecorder.assertTrue(0) { it is AddRoomToSpaceEvent.UpdateSearchVisibleRange } + } } private fun AndroidComposeTestRule.setAddRoomToSpaceView( diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/LazyListState.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/LazyListState.kt index be6888642b..f252c69b8f 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/LazyListState.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/LazyListState.kt @@ -12,17 +12,11 @@ import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.lazy.LazyListLayoutInfo import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState 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. @@ -79,20 +73,3 @@ suspend fun LazyListState.animateScrollToItemCenter(index: Int) { 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) - } - } -} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/OnVisibleRangeChangeEffect.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/OnVisibleRangeChangeEffect.kt new file mode 100644 index 0000000000..01a3be1711 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/OnVisibleRangeChangeEffect.kt @@ -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) + } + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/DynamicRoomList.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/DynamicRoomList.kt index af2c083d05..952cee6f22 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/DynamicRoomList.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/DynamicRoomList.kt @@ -8,8 +8,6 @@ package io.element.android.libraries.matrix.api.roomlist -import timber.log.Timber - /** * RoomList with dynamic filtering and loading. * This is useful for large lists of rooms. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomList.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomList.kt index 2b343bd625..a0d092596a 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomList.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomList.kt @@ -21,7 +21,6 @@ import kotlin.time.Duration * Can be retrieved from [RoomListService] methods. */ interface RoomList { - /** * The loading state of the room list. */ diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt index 4905471e98..5fd5e0c75d 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt @@ -18,13 +18,11 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.callbackFlow 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.RoomListDynamicEntriesController +import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind import org.matrix.rustcomponents.sdk.RoomListEntriesListener import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate -import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind import org.matrix.rustcomponents.sdk.RoomListInterface import org.matrix.rustcomponents.sdk.RoomListLoadingState import org.matrix.rustcomponents.sdk.RoomListLoadingStateListener diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt index 0b92e3acad..1d90b07104 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt @@ -90,8 +90,6 @@ internal class RoomListFactory( } } - - private fun RoomListLoadingState.toLoadingState(): RoomList.LoadingState { return when (this) { is RoomListLoadingState.Loaded -> RoomList.LoadingState.Loaded(maximumNumberOfRooms?.toInt() ?: 0) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilterMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilterMapper.kt index e807501451..fdee790b19 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilterMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilterMapper.kt @@ -27,7 +27,6 @@ import org.matrix.rustcomponents.sdk.RoomListFilterCategory * Mapper for converting RoomListFilter to Rust SDK filter kinds. */ internal object RoomListFilterMapper { - /** * Base rust filters to always apply across all room lists. * These filters ensure we show: diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustDynamicRoomList.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustDynamicRoomList.kt index 241d33086d..0d7b2394b3 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustDynamicRoomList.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustDynamicRoomList.kt @@ -27,7 +27,6 @@ internal class RustDynamicRoomList( private val dynamicController: () -> RoomListDynamicEntriesController?, private val addPagesCount: Int = DEFAULT_ADD_PAGES_COUNT ) : DynamicRoomList { - private val mutex = Mutex() override suspend fun rebuildSummaries() { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimplePagedRoomList.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeDynamicRoomList.kt similarity index 52% rename from libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimplePagedRoomList.kt rename to libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeDynamicRoomList.kt index dacd835591..ffb75a535d 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimplePagedRoomList.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeDynamicRoomList.kt @@ -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.RoomSummary import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.getAndUpdate -data class SimplePagedRoomList( - override val summaries: MutableStateFlow>, - override val loadingState: StateFlow, - private val currentFilter: MutableStateFlow +class FakeDynamicRoomList( + override val summaries: MutableStateFlow> = MutableStateFlow(emptyList()), + override val loadingState: MutableStateFlow = MutableStateFlow(RoomList.LoadingState.NotLoaded), + override val pageSize: Int = Int.MAX_VALUE, + val currentFilter: MutableStateFlow = 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 { - override val pageSize: Int = Int.MAX_VALUE - private val loadedPages = MutableStateFlow(1) - override suspend fun loadMore() { - // No-op - loadedPages.getAndUpdate { it + 1 } + loadMoreLambda() } override suspend fun reset() { - loadedPages.emit(1) + resetLambda() } override suspend fun updateFilter(filter: RoomListFilter) { - currentFilter.emit(filter) + updateFilterLambda(filter) } override suspend fun rebuildSummaries() { - // No-op + rebuildSummariesLambda() } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt index dc2faae91a..cdac4cd16a 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt @@ -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.roomlist.DynamicRoomList 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.RoomSummary import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow class FakeRoomListService( - var subscribeToVisibleRoomsLambda: (List) -> Unit = {}, + private val subscribeToVisibleRoomsLambda: (List) -> Unit = {}, + private val createRoomListLambda: (pageSize: Int) -> DynamicRoomList = { pageSize -> FakeDynamicRoomList(pageSize = pageSize) }, + override val allRooms: RoomList = createRoomListLambda(Int.MAX_VALUE), ) : RoomListService { - private val allRoomSummariesFlow = MutableStateFlow>(emptyList()) - private val allRoomsLoadingStateFlow = MutableStateFlow(RoomList.LoadingState.NotLoaded) private val roomListStateFlow = MutableStateFlow(RoomListService.State.Idle) private val syncIndicatorStateFlow = MutableStateFlow(RoomListService.SyncIndicator.Hide) - suspend fun postAllRooms(roomSummaries: List) { - allRoomSummariesFlow.emit(roomSummaries) - } - - suspend fun postAllRoomsLoadingState(loadingState: RoomList.LoadingState) { - allRoomsLoadingStateFlow.emit(loadingState) - } - suspend fun postState(state: RoomListService.State) { roomListStateFlow.emit(state) } @@ -46,22 +36,12 @@ class FakeRoomListService( pageSize: Int, source: RoomList.Source, coroutineScope: CoroutineScope, - ): DynamicRoomList { - return when (source) { - RoomList.Source.All -> allRooms - } - } + ) = createRoomListLambda(pageSize) override suspend fun subscribeToVisibleRooms(roomIds: List) { subscribeToVisibleRoomsLambda(roomIds) } - override val allRooms = SimplePagedRoomList( - allRoomSummariesFlow, - allRoomsLoadingStateFlow, - MutableStateFlow(RoomListFilter.all()) - ) - override val state: StateFlow = roomListStateFlow override val syncIndicator: StateFlow = syncIndicatorStateFlow diff --git a/libraries/roomselect/impl/build.gradle.kts b/libraries/roomselect/impl/build.gradle.kts index 1a5ae81928..07443bbf9d 100644 --- a/libraries/roomselect/impl/build.gradle.kts +++ b/libraries/roomselect/impl/build.gradle.kts @@ -16,6 +16,12 @@ plugins { android { namespace = "io.element.android.libraries.roomselect.impl" + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } } setupDependencyInjection() @@ -30,6 +36,6 @@ dependencies { implementation(projects.libraries.uiStrings) api(projects.libraries.roomselect.api) - testCommonDependencies(libs) + testCommonDependencies(libs, includeTestComposeView = true) testImplementation(projects.libraries.matrix.test) } diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectEvents.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectEvents.kt index e6dbe63177..a3512c419c 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectEvents.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectEvents.kt @@ -16,5 +16,5 @@ sealed interface RoomSelectEvents { // TODO remove to restore multi-selection data object RemoveSelectedRoom : RoomSelectEvents data object ToggleSearchActive : RoomSelectEvents - data class UpdateVisibleRange(val range: IntRange): RoomSelectEvents + data class UpdateVisibleRange(val range: IntRange) : RoomSelectEvents } diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStateProvider.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStateProvider.kt index 6cd2bc6921..b0d18659b6 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStateProvider.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStateProvider.kt @@ -43,22 +43,23 @@ open class RoomSelectStateProvider : PreviewParameterProvider { ) } -private fun aRoomSelectState( +internal fun aRoomSelectState( mode: RoomSelectMode = RoomSelectMode.Forward, resultState: SearchBarResultState> = SearchBarResultState.Initial(), searchQuery: String = "", isSearchActive: Boolean = false, selectedRooms: ImmutableList = persistentListOf(), + eventSink: (RoomSelectEvents) -> Unit = {}, ) = RoomSelectState( mode = mode, resultState = resultState, searchQuery = TextFieldState(initialText = searchQuery), isSearchActive = isSearchActive, selectedRooms = selectedRooms, - eventSink = {} + eventSink = eventSink, ) -private fun aRoomSelectRoomList() = persistentListOf( +internal fun aRoomSelectRoomList() = persistentListOf( aSelectRoomInfo( roomId = RoomId("!room1:domain"), name = "Room with name", diff --git a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt index bd8ef59482..67c1b52231 100644 --- a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt +++ b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt @@ -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.RoomListService 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.ui.model.toSelectRoomInfo import io.element.android.libraries.roomselect.api.RoomSelectMode 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 kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -63,9 +67,12 @@ class RoomSelectPresenterTest { @Test fun `present - update query`() = runTest { val roomSummary = aRoomSummary() - val roomListService = FakeRoomListService().apply { - postAllRooms(listOf(roomSummary)) - } + val roomList = FakeDynamicRoomList( + summaries = MutableStateFlow(listOf(roomSummary)) + ) + val roomListService = FakeRoomListService( + createRoomListLambda = { roomList } + ) val presenter = createRoomSelectPresenter( roomListService = roomListService ) @@ -81,12 +88,12 @@ class RoomSelectPresenterTest { skipItems(1) initialState.searchQuery.setTextAndPlaceCursorAtEnd("string not contained") assertThat( - roomListService.allRooms.currentFilter.value + roomList.currentFilter.value ).isEqualTo( RoomListFilter.NormalizedMatchRoomName("string not contained") ) assertThat(awaitItem().searchQuery.text.toString()).isEqualTo("string not contained") - roomListService.postAllRooms( + roomList.summaries.emit( emptyList() ) assertThat(awaitItem().resultState).isInstanceOf(SearchBarResultState.NoResultsFound::class.java) @@ -96,9 +103,12 @@ class RoomSelectPresenterTest { @Test fun `present - select and remove a room`() = runTest { val roomSummary = aRoomSummary() - val roomListService = FakeRoomListService().apply { - postAllRooms(listOf(roomSummary)) - } + val roomList = FakeDynamicRoomList( + summaries = MutableStateFlow(listOf(roomSummary)) + ) + val roomListService = FakeRoomListService( + createRoomListLambda = { roomList } + ) val presenter = createRoomSelectPresenter( roomListService = roomListService, ) @@ -114,6 +124,35 @@ class RoomSelectPresenterTest { cancel() } } + + @Test + fun `present - UpdateVisibleRange triggers pagination when near end`() = runTest { + val loadMoreLambda = lambdaRecorder { } + 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(