diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt index 3d59b3d922..eb4735de6e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -25,7 +25,6 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.map import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.identity.IdentityState @@ -59,9 +58,6 @@ class RoomMemberListPresenter( @Composable override fun present(): RoomMemberListState { var searchQuery by rememberSaveable { mutableStateOf("") } - var searchResults by remember { - mutableStateOf>>(SearchBarResultState.Initial()) - } val membersState by room.membersStateFlow.collectAsState() val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val canInvite by room.canInviteAsState(syncUpdateFlow.value) @@ -75,8 +71,8 @@ class RoomMemberListPresenter( .launchIn(this) } - var roomMembers: AsyncData by remember { mutableStateOf(AsyncData.Loading()) } var selectedSection by remember { mutableStateOf(SelectedSection.MEMBERS) } + var roomMembers: AsyncData by remember { mutableStateOf(AsyncData.Loading()) } var filteredRoomMembers: AsyncData by remember { mutableStateOf(AsyncData.Loading()) } // Update the room members when the screen is loaded diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt index b03faa97cb..580db7667a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.architecture.map import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.room.RoomMember @@ -21,108 +21,59 @@ import kotlinx.collections.immutable.persistentListOf internal class RoomMemberListStateProvider : PreviewParameterProvider { override val values: Sequence - get() = roomMemberListStates() + bannedRoomMemberListStates() + get() = sequenceOf( + aRoomMemberListState( + roomMembers = AsyncData.Loading(), + selectedSection = SelectedSection.MEMBERS, + ), + aRoomMemberListState( + roomMembers = AsyncData.Failure(Exception("Error details")), + selectedSection = SelectedSection.MEMBERS, + ), + aRoomMemberListState( + roomMembers = aLoadedRoomMembers(), + selectedSection = SelectedSection.MEMBERS, + ), + aRoomMemberListState( + roomMembers = aLoadedRoomMembers(), + selectedSection = SelectedSection.BANNED, + moderationState = aRoomMemberModerationState(canBan = true), + ), + aRoomMemberListState( + roomMembers = aLoadedRoomMembers(), + canInvite = true, + selectedSection = SelectedSection.MEMBERS, + ), + aRoomMemberListState( + roomMembers = aLoadedRoomMembers(), + searchQuery = "alice", + selectedSection = SelectedSection.MEMBERS, + ), + aRoomMemberListState( + roomMembers = aLoadedRoomMembers(), + searchQuery = "something-with-no-results", + selectedSection = SelectedSection.MEMBERS, + ), + ) } -private fun roomMemberListStates(): Sequence = sequenceOf( - aRoomMemberListState( - roomMembers = AsyncData.Success( - RoomMembers( - invited = persistentListOf(aVictor().withIdentity(), aWalter().withIdentity()), - joined = persistentListOf(anAlice().withIdentity(), aBob().withIdentity(), aWalter().withIdentity()), - banned = persistentListOf(), - ), +private fun aLoadedRoomMembers() = AsyncData.Success( + RoomMembers( + invited = persistentListOf( + anInvitedVictor().withIdentity(), + anInvitedWalter().withIdentity(), ), - selectedSection = SelectedSection.MEMBERS, - ), - aRoomMemberListState( - roomMembers = AsyncData.Success( - RoomMembers( - invited = persistentListOf(aVictor().withIdentity(), aWalter().withIdentity()), - joined = persistentListOf( - anAlice().withIdentity(identityState = IdentityState.Verified), - aBob().withIdentity(identityState = IdentityState.PinViolation), - aWalter().withIdentity(identityState = IdentityState.VerificationViolation) - ), - banned = persistentListOf(), - ) + joined = persistentListOf( + anAlice().withIdentity(identityState = IdentityState.Verified), + aBob().withIdentity(identityState = IdentityState.PinViolation), + aCarol().withIdentity(), + aDavid().withIdentity(), + anEve().withIdentity(identityState = IdentityState.VerificationViolation) ), - selectedSection = SelectedSection.MEMBERS, - moderationState = aRoomMemberModerationState(canBan = true) - ), - aRoomMemberListState( - roomMembers = AsyncData.Loading(), - selectedSection = SelectedSection.MEMBERS, - ), - aRoomMemberListState().copy( - canInvite = true, - selectedSection = SelectedSection.MEMBERS, - ), - aRoomMemberListState().copy( - selectedSection = SelectedSection.MEMBERS, - ), - aRoomMemberListState().copy( - selectedSection = SelectedSection.MEMBERS, - ), - aRoomMemberListState().copy( - searchQuery = "someone", - selectedSection = SelectedSection.MEMBERS, - ), - aRoomMemberListState().copy( - searchQuery = "@someone:matrix.org", - selectedSection = SelectedSection.MEMBERS, - ), - aRoomMemberListState().copy( - searchQuery = "something-with-no-results", - selectedSection = SelectedSection.MEMBERS, - ), - aRoomMemberListState( - roomMembers = AsyncData.Failure(Exception("Error details")), - selectedSection = SelectedSection.MEMBERS, - ), -) - -private fun bannedRoomMemberListStates(): Sequence = sequenceOf( - aRoomMemberListState( - roomMembers = AsyncData.Success( - RoomMembers( - invited = persistentListOf(), - joined = persistentListOf(), - banned = persistentListOf( - aRoomMember(userId = UserId("@alice:example.com"), displayName = "Alice").withIdentity(), - aRoomMember(userId = UserId("@bob:example.com"), displayName = "Bob").withIdentity(), - aRoomMember(userId = UserId("@charlie:example.com"), displayName = "Charlie").withIdentity(), - ), - ) + banned = persistentListOf( + aBannedMallory().withIdentity(), + aBannedSusie().withIdentity() ), - moderationState = aRoomMemberModerationState(), - selectedSection = SelectedSection.BANNED, - ), - aRoomMemberListState( - roomMembers = AsyncData.Loading( - RoomMembers( - invited = persistentListOf(), - joined = persistentListOf(), - banned = persistentListOf( - aRoomMember(userId = UserId("@alice:example.com"), displayName = "Alice").withIdentity(), - aRoomMember(userId = UserId("@bob:example.com"), displayName = "Bob").withIdentity(), - aRoomMember(userId = UserId("@charlie:example.com"), displayName = "Charlie").withIdentity(), - ), - ) - ), - moderationState = aRoomMemberModerationState(), - selectedSection = SelectedSection.BANNED, - ), - aRoomMemberListState( - roomMembers = AsyncData.Success( - RoomMembers( - invited = persistentListOf(), - joined = persistentListOf(), - banned = persistentListOf(), - ) - ), - moderationState = aRoomMemberModerationState(), - selectedSection = SelectedSection.BANNED, ) ) @@ -130,14 +81,17 @@ internal fun aRoomMemberListState( roomMembers: AsyncData = AsyncData.Loading(), moderationState: RoomMemberModerationState = aRoomMemberModerationState(), selectedSection: SelectedSection = SelectedSection.MEMBERS, + searchQuery: String = "", + canInvite: Boolean = false, + eventSink: (RoomMemberListEvents) -> Unit = {}, ) = RoomMemberListState( roomMembers = roomMembers, - filteredRoomMembers = roomMembers, - searchQuery = "", - canInvite = false, + filteredRoomMembers = roomMembers.map { it.filter(searchQuery) }, + searchQuery = searchQuery, + canInvite = canInvite, moderationState = moderationState, selectedSection = selectedSection, - eventSink = {} + eventSink = eventSink ) fun aRoomMemberModerationState( @@ -176,21 +130,30 @@ fun aRoomMember( fun aRoomMemberList() = persistentListOf( anAlice(), aBob(), - aRoomMember(UserId("@carol:server.org"), "Carol"), - aRoomMember(UserId("@david:server.org"), "David"), - aRoomMember(UserId("@eve:server.org"), "Eve"), - aRoomMember(UserId("@justin:server.org"), "Justin"), - aRoomMember(UserId("@mallory:server.org"), "Mallory"), - aRoomMember(UserId("@susie:server.org"), "Susie"), - aVictor(), - aWalter(), + aCarol(), + aDavid(), + anEve(), + anInvitedVictor(), + anInvitedWalter(), + aBannedSusie(), + aBannedMallory(), ) +fun anEve(): RoomMember = aRoomMember(UserId("@eve:server.org"), "Eve") + +fun aDavid(): RoomMember = aRoomMember(UserId("@david:server.org"), "David") + +fun aCarol(): RoomMember = aRoomMember(UserId("@carol:server.org"), "Carol") + fun anAlice() = aRoomMember(UserId("@alice:server.org"), "Alice", role = RoomMember.Role.Admin) fun aBob() = aRoomMember(UserId("@bob:server.org"), "Bob", role = RoomMember.Role.Moderator) -fun aVictor() = aRoomMember(UserId("@victor:server.org"), "Victor", membership = RoomMembershipState.INVITE) +fun anInvitedVictor() = aRoomMember(UserId("@victor:server.org"), "Victor", membership = RoomMembershipState.INVITE) -fun aWalter() = aRoomMember(UserId("@walter:server.org"), "Walter", membership = RoomMembershipState.INVITE) +fun anInvitedWalter() = aRoomMember(UserId("@walter:server.org"), "Walter", membership = RoomMembershipState.INVITE) + +fun aBannedSusie(): RoomMember = aRoomMember(UserId("@susie:server.org"), "Susie", membership = RoomMembershipState.BAN) + +fun aBannedMallory(): RoomMember = aRoomMember(UserId("@mallory:server.org"), "Mallory", membership = RoomMembershipState.BAN) private fun RoomMember.withIdentity(identityState: IdentityState? = null) = RoomMemberWithIdentityState(this, identityState) diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt index 3c0fe34366..1548feae88 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt @@ -8,36 +8,27 @@ package io.element.android.features.roomdetails.impl.members -import app.cash.molecule.RecompositionMode -import app.cash.molecule.moleculeFlow -import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.designsystem.theme.components.SearchBarResultState -import io.element.android.libraries.matrix.api.room.BaseRoom +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.RoomMembersState +import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService 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.aRoomInfo import io.element.android.tests.testutils.WarmUpRule -import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.test import io.element.android.tests.testutils.testCoroutineDispatchers -import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.time.withTimeout -import kotlinx.coroutines.withTimeout import org.junit.Rule import org.junit.Test -import kotlin.time.Duration.Companion.seconds @ExperimentalCoroutinesApi class RoomMemberListPresenterTest { @@ -45,176 +36,132 @@ class RoomMemberListPresenterTest { val warmUpRule = WarmUpRule() @Test - fun `member loading is done automatically on start, but is async`() = runTest { - val room = FakeJoinedRoom( - baseRoom = FakeBaseRoom( - updateMembersResult = { Result.success(Unit) }, - canInviteResult = { Result.success(true) } - ).apply { - // Needed to avoid discarding the loaded members as a partial and invalid result - givenRoomInfo(aRoomInfo(joinedMembersCount = 2)) - } - ) - val presenter = createPresenter(joinedRoom = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + fun `initial state is loading`() = runTest { + val presenter = createPresenter() + presenter.test { skipItems(1) val initialState = awaitItem() - assertThat(initialState.roomMembers.isLoading()).isTrue() + assertThat(initialState.filteredRoomMembers.isLoading()).isTrue() assertThat(initialState.searchQuery).isEmpty() - assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java) - assertThat(initialState.isSearchActive).isFalse() - room.givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) - // Skip item while the new members state is processed - skipItems(1) - val loadedMembersState = awaitItem() - assertThat(loadedMembersState.roomMembers.isLoading()).isFalse() - assertThat(loadedMembersState.roomMembers.dataOrNull()?.invited) - .isEqualTo(listOf(RoomMemberWithIdentityState(aVictor(), null), RoomMemberWithIdentityState(aWalter(), null))) - assertThat(loadedMembersState.roomMembers.dataOrNull()?.joined).isNotEmpty() + assertThat(initialState.selectedSection).isEqualTo(SelectedSection.MEMBERS) } } @Test - fun `member loading is done automatically when RoomInfo's activeMemberCount changes`() = runTest { - val reloadMembersMutex = Mutex() - val updateMembersLambda = lambdaRecorder { - if (reloadMembersMutex.isLocked) { - reloadMembersMutex.unlock() + fun `hide banned section when there is no banned users`() = runTest { + val allRoomMembers = aRoomMemberList() + val noBannedMembers = allRoomMembers + .filterNot { it.membership == RoomMembershipState.BAN } + .toImmutableList() + val room = createFakeJoinedRoom() + .apply { + givenRoomMembersState(RoomMembersState.Ready(allRoomMembers)) } - } - val room = FakeJoinedRoom( - baseRoom = FakeBaseRoom( - updateMembersResult = updateMembersLambda, - canInviteResult = { Result.success(true) } - ).apply { - // Needed to avoid discarding the loaded members as a partial and invalid result - givenRoomInfo(aRoomInfo(joinedMembersCount = 2)) - } - ) - val presenter = createPresenter(joinedRoom = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - val initialState = awaitItem() - assertThat(initialState.roomMembers.isLoading()).isTrue() - room.givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) - // Skip item while the new members state is processed - skipItems(1) - val loadedMembersState = awaitItem() - assertThat(loadedMembersState.roomMembers.isLoading()).isFalse() - assertThat(loadedMembersState.roomMembers.dataOrNull()?.joined).isNotEmpty() - - // Assert no events are emitted only with that change - expectNoEvents() - - // This will only progress if the `Room.updateMembers()` function is called, triggered by the RoomInfo change - withTimeout(10.seconds) { - reloadMembersMutex.withLock { - launch { room.givenRoomInfo(aRoomInfo(activeMembersCount = 0L)) } - } - } - - // Update the room members state as `Room.updateMembers()` would have done with the actual implementation - room.givenRoomMembersState(RoomMembersState.Ready(persistentListOf())) - // Wait for another update - skipItems(1) - // The members should be reloaded now - assertThat(awaitItem().roomMembers.dataOrNull()?.joined).isEmpty() - } - } - - @Test - fun `open search`() = runTest { val presenter = createPresenter( - joinedRoom = FakeJoinedRoom( - baseRoom = FakeBaseRoom( - updateMembersResult = { Result.success(Unit) }, - canInviteResult = { Result.success(true) } - ) - ) + joinedRoom = room, + roomMemberModerationState = aRoomMemberModerationState(canBan = true), ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { skipItems(1) val loadedState = awaitItem() - loadedState.eventSink(RoomMemberListEvents.OnSearchActiveChanged(true)) + assertThat(loadedState.showBannedSection).isTrue() + loadedState.eventSink(RoomMemberListEvents.ChangeSelectedSection(SelectedSection.BANNED)) + val bannedSectionState = awaitItem() + assertThat(bannedSectionState.selectedSection).isEqualTo(SelectedSection.BANNED) + // Now update the room members to have no banned users + room.givenRoomMembersState(RoomMembersState.Ready(noBannedMembers)) skipItems(1) - val searchActiveState = awaitItem() - assertThat(searchActiveState.isSearchActive).isTrue() + val noBannedMembersState = awaitItem() + assertThat(noBannedMembersState.showBannedSection).isFalse() + skipItems(1) + val finalState = awaitItem() + assertThat(finalState.selectedSection).isEqualTo(SelectedSection.MEMBERS) + } + + } + + @Test + fun `member loading is done automatically on start, but is async`() = runTest { + val room = createFakeJoinedRoom() + val presenter = createPresenter(joinedRoom = room) + presenter.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.filteredRoomMembers.isLoading()).isTrue() + assertThat(initialState.searchQuery).isEmpty() + room.givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) + // Skip items while the new members state is processed + skipItems(2) + val loadedState = awaitItem() + val loadedRoomMembers = loadedState.filteredRoomMembers.dataOrNull()!! + assertThat(loadedRoomMembers.joined).isNotEmpty() + assertThat(loadedRoomMembers.banned).isNotEmpty() + assertThat(loadedRoomMembers.invited).isNotEmpty() + assertThat(loadedRoomMembers.isEmpty(SelectedSection.MEMBERS)).isFalse() + assertThat(loadedRoomMembers.isEmpty(SelectedSection.BANNED)).isFalse() } } @Test fun `search for something which is not found`() = runTest { - val presenter = createPresenter( - joinedRoom = FakeJoinedRoom( - baseRoom = FakeBaseRoom( - updateMembersResult = { Result.success(Unit) }, - canInviteResult = { Result.success(true) } - ) - ) - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + val room = createFakeJoinedRoom().apply { + givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) + } + val presenter = createPresenter(joinedRoom = room) + presenter.test { skipItems(1) val loadedState = awaitItem() - loadedState.eventSink(RoomMemberListEvents.OnSearchActiveChanged(true)) - val searchActiveState = awaitItem() - searchActiveState.eventSink(RoomMemberListEvents.UpdateSearchQuery("something")) - skipItems(1) + val loadedRoomMembers = loadedState.filteredRoomMembers.dataOrNull()!! + assertThat(loadedRoomMembers.joined).isNotEmpty() + assertThat(loadedRoomMembers.banned).isNotEmpty() + assertThat(loadedRoomMembers.invited).isNotEmpty() + assertThat(loadedRoomMembers.isEmpty(SelectedSection.MEMBERS)).isFalse() + assertThat(loadedRoomMembers.isEmpty(SelectedSection.BANNED)).isFalse() + loadedState.eventSink(RoomMemberListEvents.UpdateSearchQuery("something")) val searchQueryUpdatedState = awaitItem() assertThat(searchQueryUpdatedState.searchQuery).isEqualTo("something") val searchSearchResultDelivered = awaitItem() - assertThat(searchSearchResultDelivered.searchResults).isInstanceOf(SearchBarResultState.NoResultsFound::class.java) + val emptyRoomMembers = searchSearchResultDelivered.filteredRoomMembers.dataOrNull()!! + assertThat(emptyRoomMembers.joined).isEmpty() + assertThat(emptyRoomMembers.banned).isEmpty() + assertThat(emptyRoomMembers.invited).isEmpty() + assertThat(emptyRoomMembers.isEmpty(SelectedSection.MEMBERS)).isTrue() + assertThat(emptyRoomMembers.isEmpty(SelectedSection.BANNED)).isTrue() } } @Test fun `search for something which is found`() = runTest { - val presenter = createPresenter( - joinedRoom = FakeJoinedRoom( - baseRoom = FakeBaseRoom( - updateMembersResult = { Result.success(Unit) }, - canInviteResult = { Result.success(true) } - ) - ) - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + val room = createFakeJoinedRoom().apply { + givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) + } + val presenter = createPresenter(joinedRoom = room) + presenter.test { skipItems(1) val loadedState = awaitItem() - loadedState.eventSink(RoomMemberListEvents.OnSearchActiveChanged(true)) - val searchActiveState = awaitItem() - searchActiveState.eventSink(RoomMemberListEvents.UpdateSearchQuery("Alice")) - skipItems(1) + val loadedRoomMembers = loadedState.filteredRoomMembers.dataOrNull()!! + assertThat(loadedRoomMembers.joined).isNotEmpty() + assertThat(loadedRoomMembers.banned).isNotEmpty() + assertThat(loadedRoomMembers.invited).isNotEmpty() + assertThat(loadedRoomMembers.isEmpty(SelectedSection.MEMBERS)).isFalse() + assertThat(loadedRoomMembers.isEmpty(SelectedSection.BANNED)).isFalse() + loadedState.eventSink(RoomMemberListEvents.UpdateSearchQuery("alice")) val searchQueryUpdatedState = awaitItem() - assertThat(searchQueryUpdatedState.searchQuery).isEqualTo("Alice") + assertThat(searchQueryUpdatedState.searchQuery).isEqualTo("alice") val searchSearchResultDelivered = awaitItem() - assertThat(searchSearchResultDelivered.searchResults).isInstanceOf(SearchBarResultState.Results::class.java) - assertThat((searchSearchResultDelivered.searchResults as SearchBarResultState.Results).results.dataOrNull()!!.joined.first().roomMember.displayName) - .isEqualTo("Alice") + val emptyRoomMembers = searchSearchResultDelivered.filteredRoomMembers.dataOrNull()!! + assertThat(emptyRoomMembers.joined).isNotEmpty() + assertThat(emptyRoomMembers.banned).isEmpty() + assertThat(emptyRoomMembers.invited).isEmpty() + assertThat(emptyRoomMembers.isEmpty(SelectedSection.MEMBERS)).isFalse() + assertThat(emptyRoomMembers.isEmpty(SelectedSection.BANNED)).isTrue() } } @Test fun `present - asynchronously sets canInvite when user has correct power level`() = runTest { - val presenter = createPresenter( - joinedRoom = FakeJoinedRoom( - baseRoom = FakeBaseRoom( - canInviteResult = { Result.success(true) }, - updateMembersResult = { Result.success(Unit) } - ) - ) - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + val presenter = createPresenter() + presenter.test { skipItems(1) val loadedState = awaitItem() assertThat(loadedState.canInvite).isTrue() @@ -224,17 +171,11 @@ class RoomMemberListPresenterTest { @Test fun `present - asynchronously sets canInvite when user does not have correct power level`() = runTest { val presenter = createPresenter( - joinedRoom = FakeJoinedRoom( - baseRoom = FakeBaseRoom( - canInviteResult = { Result.success(false) }, - updateMembersResult = { Result.success(Unit) } - ) + joinedRoom = createFakeJoinedRoom( + canInviteResult = { Result.success(false) }, ) ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) + presenter.test { val loadedState = awaitItem() assertThat(loadedState.canInvite).isFalse() } @@ -243,70 +184,54 @@ class RoomMemberListPresenterTest { @Test fun `present - asynchronously sets canInvite when power level check fails`() = runTest { val presenter = createPresenter( - joinedRoom = FakeJoinedRoom( - baseRoom = FakeBaseRoom( - canInviteResult = { Result.failure(RuntimeException("Eek")) }, - updateMembersResult = { Result.success(Unit) } - ) + joinedRoom = createFakeJoinedRoom( + canInviteResult = { Result.failure(RuntimeException("Eek")) }, ) ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) + presenter.test { val loadedState = awaitItem() assertThat(loadedState.canInvite).isFalse() } } @Test - fun `present - RoomMemberSelected will open the moderation options when target user is not banned`() = runTest { - val roomMemberModerationPresenter = Presenter { - aRoomMemberModerationState(canBan = true, canKick = true) - } + fun `present - RoomMemberSelected will open the moderation options`() = runTest { val presenter = createPresenter( - roomMemberModerationPresenter = roomMemberModerationPresenter, - joinedRoom = FakeJoinedRoom( - baseRoom = FakeBaseRoom( - updateMembersResult = { Result.success(Unit) }, - canInviteResult = { Result.success(true) } - ) - ) + roomMemberModerationState = aRoomMemberModerationState(canBan = true, canKick = true) ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { skipItems(1) - awaitItem().eventSink(RoomMemberListEvents.RoomMemberSelected(aVictor())) + awaitItem().eventSink(RoomMemberListEvents.RoomMemberSelected(anInvitedVictor())) } } } -@ExperimentalCoroutinesApi -private fun TestScope.createDataSource( - room: BaseRoom = FakeBaseRoom().apply { - givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) - }, - coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers() -) = RoomMemberListDataSource(room, coroutineDispatchers) +private fun createFakeJoinedRoom( + updateMembersResult: () -> Unit = { }, + canInviteResult: (UserId) -> Result = { Result.success(true) }, +): FakeJoinedRoom { + return FakeJoinedRoom( + baseRoom = FakeBaseRoom( + updateMembersResult = updateMembersResult, + canInviteResult = canInviteResult, + ).apply { + // Needed to avoid discarding the loaded members as a partial and invalid result + givenRoomInfo(aRoomInfo(joinedMembersCount = 2)) + } + ) +} @ExperimentalCoroutinesApi private fun TestScope.createPresenter( coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), - joinedRoom: JoinedRoom = FakeJoinedRoom( - baseRoom = FakeBaseRoom( - updateMembersResult = { Result.success(Unit) } - ) - ), - roomMemberListDataSource: RoomMemberListDataSource = createDataSource(coroutineDispatchers = coroutineDispatchers), + joinedRoom: JoinedRoom = createFakeJoinedRoom(), encryptedService: FakeEncryptionService = FakeEncryptionService(), - roomMemberModerationPresenter: Presenter = Presenter { - aRoomMemberModerationState() - }, + roomMemberModerationState: RoomMemberModerationState = aRoomMemberModerationState(), ) = RoomMemberListPresenter( room = joinedRoom, - roomMemberListDataSource = roomMemberListDataSource, coroutineDispatchers = coroutineDispatchers, - roomMembersModerationPresenter = roomMemberModerationPresenter, + roomMembersModerationPresenter = Presenter { + roomMemberModerationState + }, encryptionService = encryptedService, )